shinn_lancelot

shinn_lancelot 查看完整档案

嘉兴编辑浙江水利水电学院  |  软件 编辑XX科技  |  码农 编辑 hillpy.com 编辑
编辑

沈宝宝家的奶爸,喜欢ACGM的码农~

个人动态

shinn_lancelot 收藏了文章 · 6月24日

工作中99%能用到的git命令

【Git】工作中99%能用到的git命令

分支操作

  1. git branch 创建分支
  2. git checkout -b 创建并切换到新建的分支上
  3. git checkout 切换分支
  4. git branch 查看分支列表
  5. git branch -v 查看所有分支的最后一次操作
  6. git branch -vv 查看当前分支
  7. git brabch -b 分支名 origin/分支名 创建远程分支到本地
  8. git branch --merged 查看别的分支和当前分支合并过的分支
  9. git branch --no-merged 查看未与当前分支合并的分支
  10. git branch -d 分支名 删除本地分支
  11. git branch -D 分支名 强行删除分支
  12. git branch origin :分支名 删除远处仓库分支
  13. git merge 分支名 合并分支到当前分支上

暂存操作

  1. git stash 暂存当前修改
  2. git stash apply 恢复最近的一次暂存
  3. git stash pop 恢复暂存并删除暂存记录
  4. git stash list 查看暂存列表
  5. git stash drop 暂存名(例:stash@{0}) 移除某次暂存
  6. git stash clear 清除暂存

回退操作

  1. git reset --hard HEAD^ 回退到上一个版本
  2. git reset --hard ahdhs1(commit_id) 回退到某个版本
  3. git checkout -- file撤销修改的文件(如果文件加入到了暂存区,则回退到暂存区的,如果文件加入到了版本库,则还原至加入版本库之后的状态)
  4. git reset HEAD file 撤回暂存区的文件修改到工作区

标签操作

  1. git tag 标签名 添加标签(默认对当前版本)
  2. git tag 标签名 commit_id 对某一提交记录打标签
  3. git tag -a 标签名 -m '描述' 创建新标签并增加备注
  4. git tag 列出所有标签列表
  5. git show 标签名 查看标签信息
  6. git tag -d 标签名 删除本地标签
  7. git push origin 标签名 推送标签到远程仓库
  8. git push origin --tags 推送所有标签到远程仓库
  9. git push origin :refs/tags/标签名 从远程仓库中删除标签

其它操作

常规操作

  1. git push origin test 推送本地分支到远程仓库
  2. git rm -r --cached 文件/文件夹名字 取消文件被版本控制
  3. git reflog 获取执行过的命令
  4. git log --graph 查看分支合并图
  5. git merge --no-ff -m '合并描述' 分支名 不使用Fast forward方式合并,采用这种方式合并可以看到合并记录
  6. git check-ignore -v 文件名 查看忽略规则
  7. git add -f 文件名 强制将文件提交

git创建项目仓库

1、git init 初始化 2、git remote add origin url 关联远程仓库 3、git pull 4、git fetch 获取远程仓库中所有的分支到本地

忽略已加入到版本库中的文件

1、git update-index --assume-unchanged file 忽略单个文件 2、git rm -r --cached 文件/文件夹名字 (. 忽略全部文件)

取消忽略文件

git update-index --no-assume-unchanged file

拉取、上传免密码

git config --global credential.helper store

查看原文

shinn_lancelot 收藏了文章 · 1月16日

WebGL入门demo

WebGL入门demo

three.js入门

开场白

哇哦,绘制气球耶,在网页上?对啊!厉害了!3D效果图也能在网页上绘制出来啊,这么好玩的事情,赶紧来看看!

这里是属于WebGL的应用,webGL可以让我们在canvas上实现3D效果。而three.js是一款webGL框架,由于其易用性被广泛应用。如果要学习webGL,抛弃那些复杂的原生接口从这款框架入手是一个不错的选择。跟着我一起走!

?:three.js参考文档 英文

?:three.js参考文档 中文

看地球咯!

哈哈,别说了。先看地球:
地球

怎么画?

首先要理清逻辑。three.js框架是个法宝,要画东西的工具,模块,材料等等里面都有,找到API去用。所以,我们只需要:

  • 一张图片,也就是包裹地球身体的那张图片,
  • 一个球模型,
  • 把图片贴到球模型上去,地球就出来了,
  • 再给球加上一些动画效果,完工!

开始画!

上面讲完了画的大致流程,现在要开始画了。但是你还需要知道,不止这么简单!远不止这么简单!你需要:

1.设置three.js渲染器-renderer

2.设置摄像机camera

3.设置场景scene br>

4.设置物体object-地球

5.设置组织者

是不是一脸懵逼?别怕,来讲个故事?

其实,就是拍电影啦。需要相机,演员(这里是地球),场景(scene),导演(group)。导演组织这些活动,导演也要看场景的,他受场景的约束,演员也要听导演的。最后拍好了戏交给渲染器(renderer)来制片,发布。

好吧,这么形象估计懂了,来,我们具体来讲讲。

一步步画:

每个元素都是再定义了之后,在初始化函数内部执行。

做准备:

用到three.js框架,要先引入以下:

<script data-original="https://threejs.org/build/three.js"></script>
<script data-original="https://threejs.org/examples/js/renderers/Projector.js"></script>
<script data-original="https://threejs.org/examples/js/renderers/CanvasRenderer.js"></script>
<script data-original="https://threejs.org/examples/js/libs/stats.min.js"></script>

画地球:

看代码:

// 加载材质
var loader = new THREE.TextureLoader();
    loader.load('https://threejs.org/examples/textures/land_ocean_ice_cloud_2048.jpg',
     function(texture) {
        //画球体 形状
        var geometry = new THREE.SphereGeometry(200, 20, 20);
        // 贴图 材质纹理
        var material = new THREE.MeshBasicMaterial({
            map: texture,
            overdraw: 0.5
        })
        // 地球
        var mesh = new THREE.Mesh(geometry, material);
        group.add(mesh);
        }

画地球需要地球外面那张图片,还需要球模型geometry。图片需要裁剪之后变成material。再用这两个元素来new地球mesh,把地球交给group.

设置场景:

var scene;
scene = new THREE.Scene();
scene.add(group);

设置分组(导演):

var group;
group = new THREE.Group();

设置相机:

var camera;
// 准备好镜头
    camera = new THREE.PerspectiveCamera(60, window.innerWidth/window.innerHeight,1,2000);//相机摆上 设置相机摆放位置 产生镜头
    camera.position.z = 500;//拍的景物离我500px

先设置一下相机,把他放到里面去。

设置渲染器:

var renderer;
renderer = new THREE.CanvasRenderer();
        renderer.setClearColor(0xffffff);//设置canvas背景颜色
        renderer.setPixelRatio(window.devicePixelRatio);
        renderer.setSize(window.innerWidth, window.innerHeight);//container展示的大小
        container.appendChild(renderer.domElement)//追加 【canvas】 元素到 【container】 元素中
        stats = new Stats();
        container.appendChild(stats.dom);

先设置一下渲染器,设置在画布上面显示的属性,再把画布添加到浏览器页面上面去。还有在动画过程中的循环渲染在下面讲解。

加动画啦!

var container,stas;
var mouseX=0,mouseY=0;
var windowHalfX=window.innerWidth/2;
var windowHalfY=window.innerHeight/2;
animate();
document.addEventListener('mousemove', onDocumentMouseMove, false);//用鼠标拖
window.addEventListener('resize',onWindowResize,false);

function onDocumentMouseMove (event) {
    mouseX = event.clientX - windowHalfX;//鼠标基于中心点的偏移量;
    mouseY = event.clientY - windowHalfY;//鼠标基于中心点的偏移量;
}

function onWindowResize (event) {
    windowHalfX = window.innerWidth / 2;
    windowHalfY = window.innerHeight / 2;
    camera.aspect = window.innerWidth / window.innerHeight;
    camera.updateProjectionMatrix();
    renderer.setSize(window.innerWidth,window.innerHeight);
}

function animate () {
// 每秒60针递归调用 使地球旋转
    requestAnimationFrame(animate);
    render();
    stats.update();
}
function render () {
    camera.position.x
     += (mouseX-camera.position.x)*0.05;//在x轴上,相机根据鼠标的位置移动来移动的距离
    camera.position.y 
    += (-mouseY - camera.position.y)*0.05;//在y轴上,相机根据鼠标的位置移动来移动的距离
    camera.lookAt(scene.position);//设置视野的中心坐标
    group.rotation.y -= 0.005;//让它饶着y轴旋转 (间接的得到旋转的速度)
    renderer.render(scene, camera);//将webgl视图往外输出
}

设置在鼠标动的时候监听到,而且此时camera随即改变而改变。camera要改变根据鼠标的移动来移动它的距离在函数onDocumentMouseMove中得到,而且地球要有一种远小近大的感觉。随着鼠标移动,camera变化,地球的大小也在改变,也就是前面说的远小近大的感觉。在函数onWindowResize中实现。然后地球要动画起来要调用animate函数,每秒60针递归调用 使地球旋转,然后render函数就一直在不停的循环。状态也在不停的更新。

小结:

WebGL是是一种3D绘图标准,这种绘图技术里面用了JavaScript,所以会JavaScript,走遍天下都不怕啊?

?:源码

思考好逻辑,就可以动手的!好玩就要去尝试,就在慢慢成长。大家共同进步吧!

查看原文

shinn_lancelot 收藏了文章 · 2019-08-20

瀑布流插件Masonry中文文档【翻译】

本位为Masonry官方文档翻译,原始链接

安装Install

下载

下载压缩或未压缩的masonry

  • masonry.pkgd.min.js (压缩)
  • masonry.pkgd.js (未压缩)

CDN

在unpkg直接饮用Masonry文件。

<script data-original="https://unpkg.com/masonry-layout@4/dist/masonry.pkgd.min.js"></script>
<!-- or -->
<script data-original="https://unpkg.com/masonry-layout@4/dist/masonry.pkgd.js"></script>

包管理

使用Bower安装

bower install masonry --save

使用npm安装

npm install masonry-layout

入门Getting started

HTML

在你的项目中引入Masonry.js

<script data-original="/path/to/masonry.pkgd.min.js"></script>

Masonry的运行需要一个包含一些列子组件的grid容器标签

<div class="grid">
  <div class="grid-item">...</div>
  <div class="grid-item grid-item--width2">...</div>
  <div class="grid-item">...</div>
  ...
</div>

CSS

你可以通过CSS控制所有组件的尺寸

.grid-item { width: 200px; }
.grid-item--width2 { width: 400px; }

通过jQuery初始化

你可以将Masonry作为一个jQuery插件来使用$('selector').masonry()

$('.grid').masonry({
  // options
  itemSelector: '.grid-item',
  columnWidth: 200
});

通过原生JavaScript初始化

你可以通过原生JS使用Masonry:new Masonry( elem, options )。构造函数Masonry()接收两个参数:容器标签和配置对象。

var elem = document.querySelector('.grid');
var msnry = new Masonry( elem, {
  // options
  itemSelector: '.grid-item',
  columnWidth: 200
});
// element argument can be a selector string
//   for an individual element
var msnry = new Masonry( '.grid', {
  // options
});

在HTML中初始化

你也可以在HTML中初始化Masonry,这样不需要书写任何JavaScript。在容器标签中增加data-masonry属性,其值则是Masonry组件的配置

<div class="grid" data-masonry='{ "itemSelector": ".grid-item", "columnWidth": 200 }'>

注意:在HTML中必须以JSON格式配置,key必须带引号,例如:"itemSelector":data-masonry的值使用单引号,JSON使用双引号。
在Masonry v3版本,HTML初始化需要使用特定的class: js-masonry ,设置配置需要属性data-masonry-options,在Masonry v4之后版本中,这种写法也是兼容的。

<div class="grid js-masonry"
  data-masonry-options='{ "itemSelector": ".grid-item", "columnWidth": 200 }'>

布局Layout

组件尺寸

你可以通过CSS控制组件的尺寸

div class="grid">
  <div class="grid-item"></div>
  <div class="grid-item grid-item--width2"></div>
  <div class="grid-item grid-item--height2"></div>
  ...
</div>
.grid-item {
  float: left;
  width: 80px;
  height: 60px;
  border: 2px solid hsla(0, 0%, 0%, 0.5);
}

.grid-item--width2 { width: 160px; }
.grid-item--height2 { height: 140px; }

响应式布局

组件的尺寸可设置成百分比从而适应响应式的布局,在设置masonry布局模式时,将columnWidth设置为指定组件的尺寸,设置percentPosition: true 。组件的位置将不再改变,window改变大小事,组件将以百分比的形式响应式布局。

<div class="grid">
  <!-- width of .grid-sizer used for columnWidth -->
  <div class="grid-sizer"></div>
  <div class="grid-item"></div>
  <div class="grid-item grid-item--width2"></div>
  ...
</div>
/* fluid 5 columns */
.grid-sizer,
.grid-item { width: 20%; }
/* 2 columns */
.grid-item--width2 { width: 40%; }
$('.grid').masonry({
  // set itemSelector so .grid-sizer is not used in layout
  itemSelector: '.grid-item',
  // use element for option
  columnWidth: '.grid-sizer',
  percentPosition: true
})

imagesLoaded

Masonry排列未加载完成的图片时会导致元素的重叠,imagesLoaded可以解决这个问题。imagesLoaded是一个独立的脚本插件,你可以在imagesloaded.desandro.com下载。
初始化Masonry,在每一张图片加载完成后触发布局。

// init Masonry
var $grid = $('.grid').masonry({
  // options...
});
// layout Masonry after each image loads
$grid.imagesLoaded().progress( function() {
  $grid.masonry('layout');
});

或者在所有图片都加载完成后初始化Masonry

var $grid = $('.grid').imagesLoaded( function() {
  // init Masonry after all images have loaded
  $grid.masonry({
    // options...
  });
});

配置项Options

除了columnWidthitemSelector以外,所有的配置项都是可以选择的。

// jQuery
$('.grid').masonry({
  columnWidth: 200,
  itemSelector: '.grid-item'
});
// vanilla JS (原生JS)
var msnry = new Masonry( '.grid', {
  columnWidth: 200,
  itemSelector: '.grid-item'
});
<!-- in HTML -->
<div class="grid" data-masonry='{ "columnWidth": 200, "itemSelector": ".grid-item" }'>

必选配置项Recommended

itemSelector
用于指定参与布局的子组件。
我们建议始终设置项,用于区分参组件元素是否参与布局。

itemSelector: '.grid-item'
<div class="grid">
  <!-- do not use banner in Masonry layout -->
  <!—在Masonry布局时忽略 banner
  <div class="static-banner">Static banner</div>
  <div class="grid-item"></div>
  <div class="grid-item"></div>
  ...
</div>

columnWidth
用于在水平网格上对齐组件

我们建议设置columnWidth,如果没有设置columnWidth,Masonry将使用第一个组件的外宽作为默认值。

columnWidth: 80

使用组件尺寸,在响应式布局中奖组件的宽度设置成百分比,设置columnWidth时,将值设置为指定组件选择器的字符串,即使用该组件的外宽。

div class="grid">
  <!-- .grid-sizer empty element, only used for element sizing -->
  <div class="grid-sizer"></div>
  <div class="grid-item"></div>
  <div class="grid-item grid-item--width2"></div>
  ...
</div>
/* fluid 5 columns */
.grid-sizer,
.grid-item { width: 20%; }
/* 2 columns wide */
.grid-item--width2 { width: 40%; }
// use outer width of grid-sizer for columnWidth
columnWidth: '.grid-sizer',
itemSelector: '.grid-item',
percentPosition: true

布局Layout

组件尺寸Element sizing
尺寸配置项columnWidthgutter可以可以设置组件的列宽和间距。

<div class="grid">
  <!-- .grid-sizer empty element, only used for element sizing -->
  <div class="grid-sizer"></div>
  <div class="grid-item"></div>
  <div class="grid-item grid-item--width2"></div>
  ...
</div>
/* fluid 5 columns */
.grid-sizer,
.grid-item { width: 20%; }
/* 2 columns wide */
.grid-item--width2 { width: 40%; }
// use outer width of grid-sizer for columnWidth
columnWidth: '.grid-sizer',
// do not use .grid-sizer in layout
itemSelector: '.grid-item',
percentPosition: true

该配置项可以设置为一个选择器的字符串或一个元素

// set to a selector string
// first matching element within container element will be used
columnWidth: '.grid-sizer'

// set to an element
columnWidth: $grid.find('.grid-sizer')[0]

组件尺寸允许你使用CSS控制Masonry的布局。这在响应式布局和媒体查询中非常有用。

/* 3 columns by default */
.grid-sizer { width: 33.333%; }

@media screen and (min-width: 768px) {
  /* 5 columns for larger screens */
  .grid-sizer { width: 20%; }
}

Gutter(间距)

    gutter: 10

在js配置项gutter可以设置组件的横向间距,使用CSS margin可设置组件的纵向间距。

gutter: 10

css:

.grid-item {
  margin-bottom: 10px;
}

在响应式布局中使用组件尺寸将宽度设置为百分比时,可以将gutter的值设置为选择器字符串或者一个元素。

<div class="grid">
  <div class="grid-sizer"></div>
  <div class="gutter-sizer"></div>
  <div class="grid-item"></div>
  <div class="grid-item grid-item--width2"></div>
  ...
</div>
.grid-sizer,
.grid-item { width: 22%; }
.gutter-sizer { width: 4%; }
.grid-item--width2 { width: 48%; }
columnWidth: '.grid-sizer',
gutter: '.gutter-sizer',
itemSelector: '.grid-item',
percentPosition: true

horizontalOrder
使组件按照从左到右排列。(对比组件们在第二排的排列)

horizontalOrder: true

clipboard.png
clipboard.png

percentPosition
设置组件的位置(尺寸)为百分比而非像素数。percentPosition: true可以使宽度为百分比的组件不改变他们原本的位置。

// set positions with percent values
percentPosition: true,
columnWidth: '.grid-sizer',
itemSelector: '.grid-item'
/* fluid 5 columns */
.grid-sizer,
.grid-item { width: 20%; }

Stamp
指定组件为stamp。Masonry在布局时会避开这些组件。
配置项stamp只在Masonry实例第一次初始化完成后黏贴指定组件,但你可以通过stamp method更改后续的stamp布局。

<div class="grid">
  <div class="stamp stamp1"></div>
  <div class="stamp stamp2"></div>
  <div class="grid-item"></div>
  <div class="grid-item"></div>
  ....
</div>
// specify itemSelector so stamps do get laid out
itemSelector: '.grid-item',
// stamp elements
stamp: '.stamp'
/* position stamp elements with CSS */
.stamp {
  position: absolute;
  background: orange;
  border: 4px dotted black;
}
.stamp1 {
  left: 30%;
  top: 10px;
  width: 20%;
  height: 100px;
}
.stamp2 {
  right: 10%;
  top: 20px;
  width: 70%;
  height: 30px;
}

fitWidth

根据父级容器的尺寸,设置容器的宽,以适应可用的列数。启用之后将容器grid居中

fitWidth: true
/* center container with CSS */
.grid {
  margin: 0 auto;
}

originLeft
控制水平布局,默认状态下originLeft: true控件从左到右布局,设置originLeft: false后,控件从右向左布局。
originLeftMasonry v3使用isOriginLeft,在Masonry v4之后isOriginLeft也是被兼容的。

originLeft: false

originTop
类似originLeft,开启originTop: false后,自下而上布局

设置(Setup)

containerStyle
设置父级容器grid的css样式。默认状态下为position:’relative’,禁用grid容器的所有样式:containerStyle:null

// default
// containerStyle: { position: 'relative' }

// disable any styles being set on container
// useful if using absolute position on container
containerStyle: null

transitionDuration
控件改变位置或重排的缓动时间。默认为0.4s

// fast transitions
transitionDuration: '0.2s'

// slow transitions
transitionDuration: '0.8s'

// no transitions
transitionDuration: 0

stagger
控件重排的时间。当一个控件改变了位置,其他控件逐次的改变位置进行重排,stagger属性即为每个控件发生重排的缓动时间。,默认为值30(毫秒)

stagger: 30

resize
当窗口大小改变时控件重排以适应父级容器大小。默认为允许重排resize: true,在v3版本中使用isResizeBound,并在v4版本下兼容。

// disable window resize behavior
resize: false

/* grid has fixed width */
.grid {
  width: 320px;
}

initLayout
允许初始化布局,默认开启。
设置为initLayout: false,可以禁止初始化布局,你可以通过methods或者增加event事件的方法开启布局。V3版本使用isInitLayout

var $grid = $('.grid').masonry({
  // disable initial layout
  initLayout: false,
  //...
});
// bind event
$grid.masonry( 'on', 'layoutComplete', function() {
  console.log('layout is complete');
});
// trigger initial layout
$grid.masonry();

方法(Methods)

Methods是Masonry实例的行为
使用jQuery时,methods遵从jQuery格式.masonry( 'methodName' /* arguments */ )

$grid.masonry()
  .append( elem )
  .masonry( 'appended', elem )
  // layout
  .masonry();

原生JS的method使用类似:masonry.methodName( /* arguments */ ),与jQuery不同,原生JS不能使用链(chaining).

// vanilla JS
var msnry = new Masonry( '.grid', {...});
gridElement.appendChild( elem );
msnry.appended( elem );
msnry.layout();

布局(Layout)

layout / .masonry()
将所有组件布局。layout用于当一个组件改变了尺寸后所有的控件需要重新布局。

// jQuery
$grid.masonry()
// vanilla JS
msnry.layout()
var $grid = $('.grid').masonry({
  columnWidth: 80
});
// change size of item by toggling gigante class
$grid.on( 'click', '.grid-item', function() {
  $(this).toggleClass('gigante');
  // trigger layout after item size changes
  $grid.masonry('layout');
});

layoutItems
布局指定控件

// jQuery
$grid.masonry( 'layoutItems', items, isStill )
// vanilla JS
msnry.layoutItems( items, isStill )

items Masonry控件的数组
isStill布尔型,禁止变换
stamp
在排列中黏贴指定控件,Masonry会围绕被黏贴的元素进行排列

// jQuery
$grid.masonry( 'stamp', elements )
// vanilla JS
msnry.stamp( elements )

elements element,jQuery对象,节点,或数组
设置不参与瀑布流布局的对象,以选择器形式给出。

var $grid = $('.grid').masonry({
  // specify itemSelector so stamps do get laid out
  itemSelector: '.grid-item',
  columnWidth: 80
});
var $stamp = $grid.find('.stamp');
var isStamped = false;

$('.stamp-button').on( 'click', function() {
  // stamp or unstamp element
  if ( isStamped ) {
    $grid.masonry( 'unstamp', $stamp );
  } else {
    $grid.masonry( 'stamp', $stamp );
  }
  // trigger layout
  $grid.masonry('layout');
  // set flag
  isStamped = !isStamped;
});

unstamp
解除指定元素的stamp 状态。

增加&移除控件

Appended
在瀑布流末尾增加新控件并重排。

// jQuery
$grid.masonry( 'appended', elements )
// vanilla JS
msnry.appended( elements )

elements element,jQuery对象,节点,或数组

$('.append-button').on( 'click', function() {
  // create new item elements
  var $items = $('<div class="grid-item">...</div>');
  // append items to grid
  $grid.append( $items )
    // add and lay out newly appended items
    .masonry( 'appended', $items );
});

*(注意链chaining的使用,此处为先增加节点,再讲节点重排)
jQuery可以使用,增加字符串结构的HTML节点,但是masonry不行,所以当时用jQuery ajax动态加载节点时要将HTML节点转化成jQuery对象。

// does not work
$.get( 'page2', function( content ) {
  // HTML string added, but items not added to Masonry
  $grid.append( content ).masonry( 'appended', content );
});

// does work
$.get( 'page2', function( content ) {
  // wrap content in jQuery object
  var $content = $( content );
  // add jQuery object
  $grid.append( $content ).masonry( 'appended', $content );
});

prepended
类似append,在顶部增加新节点并重排。
addItems
项Masonry实例中增加控件元素,addItems不能像append和prepended重排新增加的元素

// jQuery
$grid.masonry( 'addItems', elements )
// vanilla JS
msnry.addItems( elements )

remove
从Masonry实例和DOM中移除元素

// jQuery
$grid.masonry( 'remove', elements )
// vanilla JS
msnry.remove( elements )
$grid.on( 'click', '.grid-item', function() {
  // remove clicked element
  $grid.masonry( 'remove', this )
    // layout remaining item elements
    .masonry('layout');
});

事件(Events)

on
增加一个Masonry事件监听。

// jQuery
var msnry = $grid.masonry( 'on', eventName, listener )
// vanilla JS
msnry.on( eventName, listener )

eventName 字符串,Masonry事件名称
listener 方法
off
移除Masonry事件

// jQuery
var msnry = $grid.masonry( 'off', eventName, listener )
// vanilla JS
msnry.off( eventName, listener )

eventName 字符串,Masonry事件名称
listener 方法
once
增加一个Masonry事件,只触发一次。

// jQuery
var msnry = $grid.masonry( 'once', eventName, listener )
// vanilla JS
msnry.once( eventName, listener )

eventName 字符串,Masonry事件名称
listener 方法

$grid.masonry( 'once', 'layoutComplete', function() {
  console.log('layout is complete, just once');
})

Utilities

reloadItems
Recollects all item elements.
For frameworks like Angular and React, reloadItems may be useful to apply changes to the DOM to Masonry.

// jQuery
$grid.masonry('reloadItems')
// vanilla JS
msnry.reloadItems()

destroy
移除所有的Masonry功能,destroy将恢复元素预加载之前的状态。

// jQuery
$grid.masonry('destroy')
// vanilla JS
msnry.destroy()
var masonryOptions = {
  itemSelector: '.grid-item',
  columnWidth: 80
};
// initialize Masonry
var $grid = $('.grid').masonry( masonryOptions );
var isActive = true;

$('.toggle-button').on( 'click', function() {
  if ( isActive ) {
    $grid.masonry('destroy'); // destroy
  } else {
    $grid.masonry( masonryOptions ); // re-initialize
  }
  // set flag
  isActive = !isActive;
});

getItemElements
返回一个组件元素的数组

// jQuery
var elems = $grid.masonry('getItemElements')
// vanilla JS
var elems = msnry.getItemElements()

elems 数组
jQuery.fn.data('masonry')
从jQuery对象中取出Masonry实例,以便读取Masonry的属性。

var msnry = $('.grid').data('masonry')
// access Masonry properties
console.log( msnry.items.length + ' filtered items'  )

Masonry.data
通过元素取出Masonry实例,Masonry.data()用于从HTML初始化的Masonry实例中取出Masonry属性。

var msnry = Masonry.data( element )

element 控件或选择器的字符串
msnry Masonry

<!-- init Masonry in HTML -->
<div class="grid" data-masonry='{...}'>...</div>
// jQuery
// pass in the element, $element[0], not the jQuery object
var msnry = Masonry.data( $('.grid')[0] )

// vanilla JS
// using an element
var grid = document.querySelector('.grid')
var msnry = Masonry.data( grid )
// using a selector string
var msnry = Masonry.data('.grid')

事件

事件绑定(event binding)

jQuery事件绑定
使用jQuery标准的事件方法绑定,如.on().off().one()

// jQuery
var $grid = $('.grid').masonry({...});

function onLayout() {
  console.log('layout done');
}
// bind event listener
$grid.on( 'layoutComplete', onLayout );
// un-bind event listener
$grid.off( 'layoutComplete', onLayout );
// bind event listener to be triggered just once. note ONE not ON
$grid.one( 'layoutComplete', function() {
  console.log('layout done, just this one time');
});

jQuery事件监听器需要一个eventargument参数,原生的JS不需要。

// jQuery, has event argument
$grid.on( 'layoutComplete', function( event, items ) {
  console.log( items.length );
});

// vanilla JS, no event argument
msnry.on( 'layoutComplete', function( items ) {
  console.log( items.length );
});

原生JS事件绑定
使用原生JS方法绑定。on(),.off(),.once()。

// vanilla JS
var msnry = new Masonry( '.grid', {...});

function onLayout() {
  console.log('layout done');
}
// bind event listener
msnry.on( 'layoutComplete', onLayout );
// un-bind event listener
msnry.off( 'layoutComplete', onLayout );
// bind event listener to be triggered just once
msnry.once( 'layoutComplete', function() {
  console.log('layout done, just this one time');
});

Masonry 事件

layoutComplete
在布局和所有位置变化完成后触发。

// jQuery
$grid.on( 'layoutComplete', function( event, laidOutItems ) {...} )
// vanilla JS
msnry.on( 'layoutComplete', function( laidOutItems ) {...} )

laidOutItems Masonry控件数组,已完成排列的控件

$grid.on( 'layoutComplete',
  function( event, laidOutItems ) {
    console.log( 'Masonry layout completed on ' +
      laidOutItems.length + ' items' );
  }
);

removeComplete
元素移除后触发

// jQuery
$grid.on( 'removeComplete', function( event, removedItems ) {...} )
// vanilla JS
msnry.on( 'removeComplete', function( removedItems ) {...} )

removedItemsMasonry控件数组,被移除的控件

$grid.on( 'removeComplete',
  function( event, removedItems ) {
    notify( 'Removed ' + removedItems.length + ' items' );
  }
);
查看原文

shinn_lancelot 收藏了文章 · 2019-08-13

阻止微信浏览器下拉滑动效果(ios11.3 橡皮筋效果)

在升级到 ios11.3 系统后,发现之前阻止页面滚动的代码e.preventDefault代码失效了。于是自己折腾了一番,找到了解决办法,分享给大家。

一、前言

浏览器在移动端有一个默认触摸滚动的效果,让我们感触最深的莫过于微信浏览器里面,下拉时自带橡皮筋的效果。

然而在开发的时候我们经常需要阻止此效果。

在此先直接给一个方案,直接加上下面的代码即可:

document.body.addEventListener('touchmove', function (e) {
  e.preventDefault(); //阻止默认的处理方式(阻止下拉滑动的效果)
}, {passive: false}); //passive 参数不能省略,用来兼容ios和android

如果不加passive:false,在 ios 上是不能起作用的。

二、解释

微信在 Android 端和 IOS 端使用的不是同样的浏览器内核:

  • Android 版 微信浏览器 :QQ浏览器 X5内核(相当于使用的 Chrome)
  • IOS 版 微信浏览器 :WKWebView(相当于使用的Safari)

所以下面分别使用 Chrome 和 Safari 来分析。

关于浏览器内核问题,有兴趣的可以看看这个:浏览器内核总结

1. Chrome 默认的事件监听参数:

clipboard.png

useCapture:false 表示事件采用冒泡机制(capture 译为 捕获),浏览器默认就是false;
passive:false 表示我现在主动告诉浏览器该监听器将使用e.preventDefault()来阻止浏览器默认的滚动行为(可以提高运行速度)。

2. Safari 默认的事件监听参数:

在 Safari 中,有一个更新

Updated root document touch event listeners to use passive mode improving scrolling performance and reducing crashes
更新了根文档触摸事件侦听器,默认使用passive:true提高滚动性能并减少崩溃

所以Safari 中默认使用了passive:true,告诉浏览器,此监听事件中,不会阻止默认的页面滚动。这将导致设置的e.preventDefault()代码失效。

所以 Safari 默认是不会阻止滚动的。

3. 结论

我们通过 e.preventDefault(); 阻止默认的下拉滑动的效果,通过添加 passive:false 参数来兼容各个浏览器。即可实现阻止移动页面滚动的功能。

三、关于 passive 参数

关于 passive 在事件监听中的作用,推荐大家看这篇文章:passive 的事件监听器

查看原文

shinn_lancelot 收藏了文章 · 2019-08-02

教你用webpack搭一个vue脚手架[超详细讲解和注释!]

1.适用人群

    1.对webpack知识有一定了解但不熟悉的同学.
    
    2.女同学!!!(233333....)

2.目的

 在自己对webpack有进一步了解的同时,也希望能帮到一些刚接触webpack的同学.
脚手架已放上github,不想听我啰嗦的同学可以直接去download或clone下来看哦.

脚手架里都有详细注释!

https://github.com/webfansplz...

觉得有帮助到你的同学给个star哈,也算是对我的一种支持!

3.脚手架结构

├── build                       构建服务和webpack配置
    |—— build.js                webpack打包服务
    |—— webpack.base.conf.js    webpack基本通用配置
    |—— webpack.dev.conf.js     webpack开发环境配置
    |—— webpack.prod.conf.js    webpack生产环境配置
├── config                      构建项目不同环境的配置
├── public                      项目打包文件存放目录
├── index.html                  项目入口文件
├── package.json                项目配置文件
├── static                       静态资源
├── .babelrc                    babel配置文件
├── .gitignore                  git忽略文件
├── postcss.config.js           postcss配置文件
├── src                         项目目录
    |—— page                    页面组件目录
    |—— router                  vue路由配置
    |—— store                   vuex配置
    |—— App.vue                 vue实例入口
    |—— main.js                 项目构建入口

4.配置npm scripts

4.1 生成package.json文件,配置npm scripts.

4.1.1 使用 npm init 命令,生成一个package.json文件!

    npm init

4.1.2 全局安装webpack和webpack-dev-server

   npm install webpack webpack-dev-server -g

4.1.3 在项目目录下安装webpack和webpack-dev-server

  npm install webpack webpack-dev-server -D

4.1.4 进入package.json配置npm scripts命令

  "scripts": {
    "dev": "webpack-dev-server  --config build/webpack.dev.conf.js",
    "start": "npm run dev",
    "build": "node build/build.js"
  }

  通过配置以上命令:
  我们可以通过npm start/npm run dev在本地进行开发,
  scripts.dev命令解读:
  通过webpack-dev-server命令 启动build文件夹下webpack.dev.conf.js。
  也可以通过npm run build 打包项目文件进行线上部署.
  scripts.build命令解读:
  通过node命令构建build文件夹下的build.js。
  命令的配置可以根据自己脚手架的配置文件位置和名称不同修改哦!

5.构建脚手架目录

同学们可以通过自己的习惯和喜爱搭建自己的脚手架目录,下面讲解以上面脚手架结构为准!

6.构建config/config.js

6.1 该文件主要用来配置构建开发环境和生产环境差异化的参数.
6.2

const _path = require("path");
const ExtractTextPlugin = require("extract-text-webpack-plugin");

//vue-loader基本配置
const baseVueLoaderConf = {
  //引入postcss插件
  postcss: {
    config: {
      path: _path.resolve("../")
    }
  },
  //转为require调用,让webpack处理目标资源!
  transformToRequire: {
    video: "src",
    source: "src",
    img: "src",
    image: "xlink:href"
  }
};

//vue-loader 开发环境配置
const devVueLoaderConf = Object.assign({}, baseVueLoaderConf, {
  //loaders
  loaders: {
    css: ["vue-style-loader", "css-loader"],
    less: ["vue-style-loader", "css-loader", "postcss-loader", "less-loader"]
  },
  cssSourceMap: true
});

//vue-loader 生产环境配置
const buildVueLoaderConf = Object.assign({}, baseVueLoaderConf, {
  //loaders
  loaders: ExtractTextPlugin.extract({
    use: ["css-loader", "postcss-loader", "less-loader"],
    fallback: "vue-style-loader"
  }),
  cssSourceMap: false
});

//开发/生产环境 配置参数!
module.exports = {
  dev: {
    publicPath: "/",
    devtoolType: "cheap-module-eval-source-map",
    vueloaderConf: devVueLoaderConf,
    host: "localhost",
    port: "1234",
    proxyTable: {}
  },
  build: {
    publicPath: "/",
    devtoolType: "source-map",
    vueloaderConf: buildVueLoaderConf,
    staticPath: "static"
  }
};

7.构建build/webpack.base.conf.js

7.1 此文件主要是webpack开发环境和生成环境的通用配置.

7.2

"use strict";

//引入node path路径模块
const path = require("path");
//引入webpack生产环境配置参数
const prodConfig = require("../config").build;

//拼接路径
function resolve(track) {
  return path.join(__dirname, "..", track);
}
//资源路径
function assetsPath(_path) {
  return path.join(prodConfig.staticPath, _path);
}

//webpack 基本设置

module.exports = {
  //项目入口文件->webpack从此处开始构建!
  entry: path.resolve(__dirname, "../src/main.js"),
  //配置模块如何被解析
  resolve: {
    //自动解析文件扩展名(补全文件后缀)(从左->右)
    // import hello from './hello'  (!hello.js? -> !hello.vue? -> !hello.json)
    extensions: [".js", ".vue", ".json"],
    //配置别名映射
    alias: {
      // import Vue from 'vue/dist/vue.esm.js'可以写成 import Vue from 'vue'
      // 键后加上$,表示精准匹配!
      vue$: "vue/dist/vue.esm.js",
      "@": resolve("src"),
      utils: resolve("src/utils"),
      components: resolve("src/components"),
      public: resolve("public")
    }
  },
  module: {
    //处理模块的规则(可在此处使用不同的loader来处理模块!)
    rules: [
      //使用babel-loader来处理src下面的所有js文件,具体babel配置在.babelrc,主要是用来转义es6
      {
        test: /\.js$/,
        use: {
          loader: "babel-loader"
        },
        include: resolve("src")
      },
      //使用url-loader(file-loader的一个再封装)对引入的图片进行编码,此处可将小于8192字节(8kb)的图片转为DataURL(base64),
      //大于limit字节的会调用file-loader进行处理!
      //图片一般发布后都是长缓存,故此处文件名加入hash做版本区分!
      {
        test: /\.(png|jpe?g|gif|svg)(\?.*)?$/,
        loader: "url-loader",
        options: {
          limit: 8192,
          name: assetsPath("img/[name].[hash:8].[ext]")
        }
      }
    ]
  }
};

8.构建 build/webpack.dev.conf.js

8.1 该文件主要用于构建开发环境

8.2

"use strict";
//引入node path路径模块
const path = require("path");
//引入webpack
const webpack = require("webpack");
//引入webpack开发环境配置参数
const devConfig = require("../config").dev;
//引入webpack基本配置
const baseConf = require("./webpack.base.conf");
//一个webpack配置合并模块,可简单的理解为与Object.assign()功能类似!
const merge = require("webpack-merge");
//一个创建html入口文件的webpack插件!
const HtmlWebpackPlugin = require("html-webpack-plugin");
//一个编译提示的webpack插件!
const FriendlyErrorsPlugin = require("friendly-errors-webpack-plugin");
//发送系统通知的一个node模块!
const notifier = require("node-notifier");
//将webpack基本配置与开发环境配置合并!
const devConf = merge(baseConf, {
  //项目出口,webpack-dev-server 生成的包并没有写入硬盘,而是放在内存中!
  output: {
    //文件名
    filename: "[name].js",
    //html引用资源路径,在dev-server中,引用的是内存中文件!
    publicPath: devConfig.publicPath
  },
  //生成sourceMaps(方便调试)
  devtool: devConfig.devtoolType,
  //
  //启动一个express服务器,使我们可以在本地进行开发!!!
  devServer: {
    //HMR控制台log等级
    clientLogLevel: "warning",
    // 热加载
    hot: true,
    //自动刷新
    inline: true,
    //自动打开浏览器
    open: true,
    //在开发单页应用时非常有用,它依赖于HTML5 history API,如果设置为true,所有的跳转将指向index.html
    historyApiFallback: true,
    //主机名
    host: devConfig.host,
    //端口号
    port: devConfig.port,
    //配置反向代理解决跨域
    proxy: devConfig.proxyTable,
    //为你的代码进行压缩。加快开发流程和优化的作用
    compress: true,
    // 在浏览器上全屏显示编译的errors或warnings。
    overlay: {
      errors: true,
      warnings: false
    },
    // 终端输出的只有初始启动信息。 webpack 的警告和错误是不输出到终端的
    quiet: true
  },
  module: {
    //处理模块的规则(可在此处使用不同的loader来处理模块!)
    rules: [
      //使用vue-loader处理以vue结尾的文件!
      {
        test: /\.vue$/,
        loader: "vue-loader",
        options: devConfig.vueloaderConf
      },
      //使用vue-style-loader!css-loader!postcss-loader处理以css结尾的文件!
      {
        test: /\.css$/,
        use: [
          "vue-style-loader",
          {
            loader: "css-loader",
            options: {
              sourceMap: true
            }
          },
          {
            loader: "postcss-loader",
            options: {
              sourceMap: true
            }
          }
        ]
      },
      //使用vue-style-loader!css-loader!postcss-loader处理以less结尾的文件!
      {
        test: /\.less$/,
        use: [
          "vue-style-loader",
          {
            loader: "css-loader",
            options: {
              sourceMap: true
            }
          },
          {
            loader: "less-loader",
            options: {
              sourceMap: true
            }
          },
          {
            loader: "postcss-loader",
            options: {
              sourceMap: true
            }
          }
        ]
      }
    ]
  },
  plugins: [
    //开启HMR(热替换功能,替换更新部分,不重载页面!)
    new webpack.HotModuleReplacementPlugin(),

    //显示模块相对路径
    new webpack.NamedModulesPlugin(),

    //编译出错时,该插件可跳过输出,确保输出资源不会包含错误!
    // new webpack.NoEmitOnErrorsPlugin(),

    //配置html入口信息
    new HtmlWebpackPlugin({
      title: "hello,xc-cli!",
      filename: "index.html",
      template: "index.html",
      //js资源插入位置,true表示插入到body元素底部
      inject: true
    }),

    //编译提示插件
    new FriendlyErrorsPlugin({
      //编译成功提示!
      compilationSuccessInfo: {
        messages: [
          `Your application is running here: http://${devConfig.host}:${devConfig.port}`
        ]
      },
      //编译出错!
      onErrors: function(severity, errors) {
        if (severity !== "error") {
          return;
        }
        const error = errors[0];
        const filename = error.file.split("!").pop();
        //编译出错时,右下角弹出错误提示!
        notifier.notify({
          title: "xc-cli",
          message: severity + ": " + error.name,
          subtitle: filename || "",
          icon: path.join(__dirname, "xc-cli.png")
        });
      }
    })
  ]
});
module.exports = devConf;

8.3 通过创建以上文件,并下载相应的依赖和创建项目入口,我们就可以通过npm run dev在本地开发vue项目啦!!!

9.创建 build/webpack.prod.conf.js

9.1 此文件主要用于构建生产环境的配置.
9.2

"use strict";
//引入node path路径模块
const path = require("path");
//引入webpack
const webpack = require("webpack");
//一个webpack配置合并模块,可简单的理解为与Object.assign()功能类似!
const merge = require("webpack-merge");
//引入webpack生产环境配置参数
const prodConfig = require("../config").build;
//引入webpack基本配置
const baseConf = require("./webpack.base.conf");
//一个创建html入口文件的webpack插件!
const HtmlWebpackPlugin = require("html-webpack-plugin");
//一个抽离出css的webpack插件!
const ExtractTextPlugin = require("extract-text-webpack-plugin");
//一个压缩css的webpack插件!
const OptimizeCSSPlugin = require("optimize-css-assets-webpack-plugin");
//一个拷贝文件的webpack插件!
const CopyWebpackPlugin = require("copy-webpack-plugin");

//资源路径
function assetsPath(_path) {
  return path.join(prodConfig.staticPath, _path);
}
//将webpack基本配置与生产环境配置合并!
const prodConf = merge(baseConf, {
  //项目出口配置
  output: {
    //Build后所有文件存放的位置
    path: path.resolve(__dirname, "../public"),
    //html引用资源路径,可在此配置cdn引用地址!
    publicPath: prodConfig.publicPath,
    //文件名
    filename: assetsPath("js/[name].[chunkhash].js"),
    //用于打包require.ensure(代码分割)方法中引入的模块
    chunkFilename: assetsPath("js/[name].[chunkhash].js")
  },
  //生成sourceMaps(方便调试)
  devtool: prodConfig.devtoolType,
  module: {
    //处理模块的规则(可在此处使用不同的loader来处理模块!)
    rules: [
      //使用vue-loader处理以vue结尾的文件!
      {
        test: /\.vue$/,
        loader: "vue-loader",
        options: prodConfig.vueloaderConf
      },
      {
        test: /\.css$/,
        use: ExtractTextPlugin.extract({
          use: ["css-loader", "postcss-loader"],
          fallback: "vue-style-loader"
        })
      },
      {
        test: /\.less$/,
        use: ExtractTextPlugin.extract({
          use: ["css-loader", "less-loader", "postcss-loader"],
          fallback: "vue-style-loader"
        })
      }
    ]
  },
  plugins: [
    //每个chunk头部添加hey,xc-cli!
    new webpack.BannerPlugin("hey,xc-cli"),

    //压缩js
    new webpack.optimize.UglifyJsPlugin({
      parallel: true,
      compress: {
        warnings: false
      }
    }),

    //分离入口引用的css,不内嵌到js bundle中!

    new ExtractTextPlugin({
      filename: assetsPath("css/[name].[contenthash].css"),
      allChunks: false
    }),

    //压缩css
    new OptimizeCSSPlugin(),

    //根据模块相对路径生成四位数hash值作为模块id
    new webpack.HashedModuleIdsPlugin(),

    //作用域提升,提升代码在浏览器执行速度
    new webpack.optimize.ModuleConcatenationPlugin(),

    //抽离公共模块,合成一个chunk,在最开始加载一次,便缓存使用,用于提升速度!

    // 1. 第三方库chunk
    new webpack.optimize.CommonsChunkPlugin({
      name: "vendor",
      minChunks: function(module) {
        //在node_modules的js文件!
        return (
          module.resource &&
          /\.js$/.test(module.resource) &&
          module.resource.indexOf(path.join(__dirname, "../node_modules")) === 0
        );
      }
    }),

    // 2. 缓存chunk
    new webpack.optimize.CommonsChunkPlugin({
      name: "manifest",
      minChunks: Infinity
    }),
    // 3.异步 公共chunk
    new webpack.optimize.CommonsChunkPlugin({
      name: "app",
      children: true,
      // (选择所有被选 chunks 的子 chunks)
      async: true,
      // (创建一个异步 公共chunk)
      minChunks: 3
      // (在提取之前需要至少三个子 chunk 共享这个模块)
    }),

    //将整个文件复制到构建输出指定目录下
    new CopyWebpackPlugin([
      {
        from: path.resolve(__dirname, "../static"),
        to: prodConfig.staticPath,
        ignore: [".*"]
      }
    ]),

    //生成html
    new HtmlWebpackPlugin({
      filename: path.resolve(__dirname, "../public/index.html"),
      template: "index.html",
      favicon: path.resolve(__dirname, "../favicon.ico"),
      //js资源插入位置,true表示插入到body元素底部
      inject: true,
      //压缩配置
      minify: {
        //删除Html注释
        removeComments: true,
        //去除空格
        collapseWhitespace: true,
        //去除属性引号
        removeAttributeQuotes: true
      },
      //根据依赖引入chunk
      chunksSortMode: "dependency"
    })
  ]
});
module.exports = prodConf;

10. 创建 build/build.js

10.1 此文件是项目打包服务,用来构建一个全量压缩包
10.2

"use strict";
//node for loading
const ora = require("ora");
// rm-rf for node
const rm = require("rimraf");
//console for node
const chalk = require("chalk");
//path for node
const path = require("path");
//webpack
const webpack = require("webpack");
//webpack production setting
const config = require("./webpack.prod.conf");
//指定删除的文件
const rmFile = path.resolve(__dirname, "../public/static");
//build start loading
const spinner = ora("building for production...");
spinner.start();

//构建全量压缩包!
rm(rmFile, function(err) {
  if (err) throw err;
  webpack(config, function(err, stats) {
    spinner.stop();
    if (err) throw err;
    process.stdout.write(
      stats.toString({
        colors: true,
        modules: false,
        children: false,
        chunks: false,
        chunkModules: false
      }) + "\n\n"
    );

    if (stats.hasErrors()) {
      console.log(chalk.red("  Build failed with errors.\n"));
      process.exit(1);
    }

    console.log(chalk.cyan("  Build complete.\n"));
    console.log(
      chalk.yellow(
        "  Tip: built files are meant to be served over an HTTP server.\n" +
          "  Opening index.html over file:// won't work.\n"
      )
    );
  });
});

10.3 创建好以上文件 我们就可以通过npm run build来打包我们的项目文件并部署上线啦。

11.大功告成!

通过以上步骤,一个spa版的vue脚手架就大功告成啦!

如果对一些细节不懂的可以留言或者上我的github查看

https://github.com/webfansplz...

最后还是那句话,如果有帮助到你,请给我star支持哈!

查看原文

shinn_lancelot 收藏了文章 · 2019-07-24

gulp4.0升级小记

前言

周日在公司的新电脑在以前gulp3.9配置的目录按下npm install时发现报了错,百度了一下得知原来gulp已经到了4.0版本,就花了一点时间去升了个级,顺便记下我个人使用到的配置文件新版本的不同点,文笔和水平有限,多多见谅

1. 新Api引入

// v3.9
let gulp = require('gulp');

// v4.0
const { series, src, dest, watch } = require('gulp');

// 新引入的src,dest可替换老版的gulp.src和gulp.dest,代码更简洁
// watch是任务监听
// series是任务按顺序执行

2. 新的创建任务方式

// 下面以压缩图片插件 gulp-imagemin 为例

let imagemin = require('gulp-imagemin');

// v3.9
gulp.task('imagemin', () => {
  gulp.src('/path')
    .pipe(imagemin())
    .pipe(gulp.dest('/path'))
})

// 4.0
function minImage() {
    return src('/path')
            .pipe(imagemin())
            .pipe(dest('/path'))
}

// 新版本使用了函数和return进行任务设置,函数名不能和引入的插件变量名称重复

3. 执行任务方式

// v3.9
gulp.task('default', [task1, task2])

// v4.0,taskFn是设置任务的函数名
function defaultTask() {
    return series(taskFn1, taskFn2, taskFn3);  // series让任务按顺序执行
}
export.default = defaultTask() // 输出控制台执行任务的名称

// 新版本的export.xxxx,这个xxxx就是在控制台中gulp执行任务的名称,可以同时export设置多个任务
// 例如export.dev= dev(),想执行dev函数中返回的任务就在控制台输入gulp dev加回车!,如果是export.build = build(),则在控制台输入gulp build加回车!,如果是export.default = default(),直接输入gulp回车即可,以此类推

4. watch和series Api

// v3.9,老版本好像要安装一个queue的插件才可以实现按顺序执行任务
let watch = require('gulp-watch');
gulp.task('watch', () => {
  gulp.watch(['filePath1', 'filePath2'], [task1, task2]);
});

// 4.0
const { watch, series} = require('gulp');
function watchTask() {
    // 注意这里不需要使用return
    watch(['filePath1', 'filePath2'], series(taskFn1, taskFn2, taskFn3));
}

// 新版本直接引入watch即可实现任务监听功能,不用安装插件
// series也可以配合watch按顺序执行设置的任务函数

5. 插件gulp-autoprefixer配置变化

// v3.9
.pipe(autoprefixer({
      browsers: ['last 2 versions'],
      cascade: false
}))

// v4.0,需要在package.json文件添加browserslist键名或者在根目录添加一个.browserslistrc文件进行gulp-autoprefixer配置
// 详情可以参考:https://github.com/browserslist/browserslist#queries
.pipe(autoprefixer())

// .browserslistrc文件
last 1 version
> 1%
maintained node versions
not dead

// package.json
"browserslist": [
   "last 1 version",
   "> 1%",
   "maintained node versions",
   "not dead"
]

其他的配置感觉新版本和老版本都是大同小异,暂时就是发现了这么多,亲测可用


后记

我是使用sass + gulp-autoprefixer进行开发的,无意发现当autoprefixer碰到-webkit-box-orient: vertical;时会自动把这个样式给删了 0.0,折腾了一番找到解决方法如下

 -webkit-line-clamp: 3;
/*! autoprefixer:innore next */  2020-02-26更新
-webkit-box-orient: vertical;
overflow: hidden;

2020-02-26更新:

1、修复gulp-px3rem(px2rem)插件生成的css文件名成为xxx.debug.css的问题

(1)找到node_modules依赖包文件夹中的gulp-px3rem文件夹
(2)进入文件夹后修改index.js中第46行和第60行的代码

46  path: file.path.replace(/(.debug)?.css$/, dpr + 'x.debug.css'), 改成 path: file.path.replace(/(.debug)?.css$/, dpr + '.css'),

60  path: file.path.replace(/(.debug)?.css$/, '.debug.css') 改成 path: file.path.replace(/(.debug)?.css$/, '.css')
(3)重启服务

2、关于使用了gulp-px3rem(px2rem)后,1px的border不能正常显示的解决方法,在css语句后添加/*no*/表示忽略该语句

.selector {
  border: 1px solid #fff;/*no*/
}

3、个人使用px2rem的方法

1、在gulefile.js的px2rem配置中设置remUnit为设计稿的宽度,默认为75
  px2rem({
    remUnit: 750 // remUnit表示rem转换比例,现在的值为750表示1rem=750px
  })
2、把html的字体大小设置成屏幕大小,下面是限制了页面的最大宽度
window.onload = function() {
  let windowWidth = document.documentElement.clientWidth;
  document.documentElement.style.fontSize = (windowWidth > 750 ? 750 : windowWidth) + 'px';
}
3、设计稿是多少px,CSS就多少px

下面是我个人的gulp4.0.2配置,希望多多指点!

查看原文

shinn_lancelot 收藏了文章 · 2019-07-15

从0搭建rollup+vue组件模板,轻松发布npm、githubpages

最近更新模板

vue-cli3携手rollup、github-actions打造自动部署的vue组件模板(使用篇)2020-01-08

前言

既然是rollup+vue组件模板,就不说明为什么采用这个模式来开发组件了。
需要了解rollup的看文档:rollup官方文档

rollup打包

找个文件夹,开干

mkdir vue-rollup-component-template
cd vue-rollup-component-template
npm init

安装 rollup

npm i -D rollup

配置文件 rollup.config.js

export default {
  input: 'src/index.js',
  output: {
    name: 'vue-rollup-component-template',
    file: 'dist/vue-rollup-component-template.js',
    format: 'umd'
  }
}

入口文件 src/index.js

const x = 1;

export default (y) => x + y

修改 package.json 命令

"scripts": {
    "build": "rollup --config rollup.config.js"
}

打包

npm run build

完成了一个简单的打包。

不同打包格式与命令

"scripts": {
    "build": "npm run build:browser && npm run build:es && npm run build:umd",
    "build:browser": "rollup --config build/rollup.config.browser.js",
    "build:es": "rollup --config build/rollup.config.es.js",
    "build:umd": "rollup --config build/rollup.config.umd.js"
},

通用配置 build/rollup.config.base.js

export default {
  input: 'src/index.js',  //入口
  plugins: [] // 插件
}
  • es – ES模块文件。

安装js压缩插件

npm i -D rollup-plugin-uglify-es

build/rollup.config.es.js

import base from './rollup.config.base'
import uglify from 'rollup-plugin-uglify-es' //js压缩

const config = Object.assign({}, base, {
  output: {
    exports: 'named',
    name: 'vue-rollup-component-template',
    file: 'dist/vue-rollup-component-template.min.js',
    format: 'iife'
  },
})

config.plugins.push(uglify())

export default config
  • umd – 通用模块定义,以amd,cjs 和 iife 为一体。

build/rollup.config.browser.js

import base from './rollup.config.base'

const config = Object.assign({}, base, {
  output: {
    exports: 'named',
    name: 'vue-rollup-component-template',
    file: 'dist/vue-rollup-component-template.umd.js',
    format: 'umd'
  },
})

export default config
  • iife – 一个自动执行的功能,适合作为< script >标签。

build/rollup.config.browser.js

import base from './rollup.config.base'
import uglify from 'rollup-plugin-uglify-es' //js压缩

const config = Object.assign({}, base, {
  output: {
    exports: 'named',
    name: 'VueRollupComponentTemplate',
    file: 'dist/vue-rollup-component-template.min.js',
    format: 'iife'
  },
})

config.plugins.push(uglify())

export default config

打包

npm run build

安装插件

组件开发少不了引入模块、es6等,需要用 插件(plugins) 在打包的关键过程中更改 Rollup 的行为。

npm i -D rollup-plugin-node-resolve rollup-plugin-commonjs
npm i -D  rollup-plugin-vue vue vue-template-compiler

修改build/rollup.config.base.js

import resolve from 'rollup-plugin-node-resolve' // 告诉 Rollup 如何查找外部模块
import commonjs from 'rollup-plugin-commonjs' // 将CommonJS模块转换为 ES2015 供 Rollup 处理
import vue from 'rollup-plugin-vue' // 处理vue文件
import babel from 'rollup-plugin-babel'  // rollup 的 babel 插件,ES6转ES5

export default {
  input: 'src/index.js',
  plugins: [
    resolve({ extensions: ['.vue'] }),
    commonjs(),
    vue(),
    babel()
  ]
}
rollup-plugin-babel 插件需要 babel 支持
npm i -D babel-core babel-preset-env babel-preset-stage-3 rollup-plugin-babel@3.0.0

新建 .babelrc

{
  "presets": [
    ["env", { "modules": false }],
    "stage-3"
  ],
}

组件开发与root设置

设置 root

npm i -D babel-plugin-module-resolver

.babelrc 增加 plugins

{
  "plugins": [
    [
      "module-resolver",
      {
        "root": ["src/"]
      }
    ]
  ]
}

从而src目录下的模块引入,不需要'../../',只要 (年龄小的、身材好的)

import VList from 'components/list'
import util from 'utils/util'
import mixins from 'mixins/mixin'
...

组件开发

src/components/list/main.vue

<template>
  <div class="v-list">
    <slot/>
    <div class="v-list-date">
      <div class="v-list-date-label">当前时间:</div>
      <div class="v-list-date-text">{{date}}</div>
    </div>
  </div>
</template>

<script>
export default {
  name: 'VList',
  data() {
    return {
      date: new Date()
    }
  }
}
</script>

src/components/list/index.js

import Main from './main';
export default Main

src/index.js

import VList from 'components/list';

const components = [VList]

const install = function (Vue) {
  components.forEach(component => {
    Vue.component(component.name, component)
  })
}

// 自动注册组件
if (typeof window !== 'undefined' && window.Vue) {
  install(window.Vue);
}

export default install

css打包压缩

npm i -D rollup-plugin-css-only  clean-css

build/rollup.config.base.js 增加

...
import css from 'rollup-plugin-css-only' // 提取css,压缩能力不行
import CleanCSS from 'clean-css' // 压缩css
import { writeFileSync } from 'fs' // 写文件

export default {
  input: 'src/index.js',
  plugins: [
    ...
    css({ output(style) {
      // 压缩 css 写入 dist/vue-rollup-component-template.css
      writeFileSync('dist/vue-rollup-component-template.css', new CleanCSS().minify(style).styles)
    } }),
    // css: false 将<style>块转换为导入语句,rollup-plugin-css-only可以提取.vue文件中的样式
    vue({ css: false }), 
    ...
  ]
}

rollup-plugin-css-only 支持 css及scss

src/components/list/main.vue

<style>
.v-list {
  width: 300px;
  margin: 0 auto;
}
</style>

<style lang="scss">
.v-list-date {
  display: flex;
  justify-content: center;
  align-items: center;
  margin-top: 30px;
  .v-list-date-label{
    font-size: 12px;
    color: #999;
  }
  .v-list-date-text{
    font-size: 12px;
    color: #666;
  }
}
</style>

<style scoped>
.v-list-1 {
  background-color: #333;
}
</style>

看起来一切顺利就可以打包发布npm了。

可能发了一堆bug到npm...,还没测试呢。

测试与docs

创建docs

其实就是 vue 的简易模板

vue init webpack-simple docs

安装依赖

cd docs
npm i

docs/src/main.js 引入、注册组件及css

...
import VueRollupComponentTemplate from '../../'
import '../../dist/vue-rollup-component-template.css'

Vue.use(VueRollupComponent)
...

docs/src/app.vue 使用组件

<template>
  <div id="app">
    <h1>vue-rollup-component-template</h1>
    <p>轻松进行组件开发、发布、展示</p>
    <v-list></v-list>
  </div>
</template>

docs/index.html js引入路径改为相对路径(加个点)

<script data-original="./dist/build.js"></script>

docs/.gitignore 把dist/忽略去掉,github展示页面需要访问dist/

...
dist/ 去掉这个
...
docs目录下只用改这几个地方就可以

准备测试

根目录下

npm i -D cross-env

package.json 添加命令

"scripts": {
    ...
    "dev": "cross-env NODE_ENV=development rollup --config build/rollup.config.es.js --watch"
},

package.json 修改/添加程序入口

...
"main": "dist/vue-rollup-component-template.umd.js",
"module": "dist/vue-rollup-component-template.esm.js",
"unpkg": "dist/vue-rollup-component-template.min.js",
...

开始测试

  • 根目录下
npm run dev
  • docs目录下
npm run dev
根目录src下内容修改会重新打包到dist,docs目录监听到dist目录变动会实时响应并刷新页面。

测试过后没问题就可以打包项目,登录npm发布组件了。

发布npm与githubpages

组件与测试是独立的。

  • 根目录下打包的组件可以直接发布npm,不用考虑测试生产环境,从而专注组件开发。
  • docs录下,在提交github之前打包,然后整个项目上传github。

发布npm

新建 .npmignore 文件,添加 npm 忽略文件

docs/
.babelrc

登录npm,然后发布

npm publish

githubpages

新建 .gitignore 文件,添加 git 忽略文件

.DS_Store
node_modules/
.vscode/
npm-debug.log
yarn-error.log

然后上传到github,打开项目设置,github pages的surce项选择docs。

打开分配的地址就可以观光页面了。

项目 github 地址

master分支是webpack3+babek6版本,如果需要webpck4+babel7版本请在 clone项目后切换 webpck4-babel7 分支
查看原文

shinn_lancelot 收藏了文章 · 2019-06-19

PHP极其强大的图片处理库Grafika详细教程(1):图像基本处理

Grafika是一个PHP图像处理库,是基于Imagick和GD,可以用于改变图片大小,剪裁,比较,添加水印等等功能。还有感知哈希,高级图像过滤,绘制贝塞尔曲线等功能,可谓非常强大。

由于功能太多,所以分成几篇文章写。

《1、图像基本处理》
《2、图像特效处理模块》
《3、图像属性处理》
《4、图形绘制》

优点:

  • 缩略图的速度非常快,质量非常高
  • 支持智能剪裁
  • 很好的支持GIF图片
  • 5种缩略图模式
  • 图像对比功能
  • 图像高级过滤功能
  • 图像混合
  • 其他图像处理库支持的API基本都支持

安装

下载

1、直接下载:

Grafika的官网Github地址

2、composer

    composer require kosinix/grafika:dev-master --prefer-dist

环境需求

  • PHP >= 5.3,当然官方推荐php7
  • GD库 >= 2.0版本
  • Imagick最好(不强求)>=3.3.0 , ImageMagick >= 6.5.3

部署

下载下来的Grafika目录基本结构像下面这样:

Grafika目录基本结构

不过composer下载下来的多一点儿,你只需要使用kosinix/grafika目录下的东西就好了。

我们在grafika目录下建立一个index.php,之后的操作都在这里。

grafika给我们提供了一个非常好用的autoloader.php位于src目录下。

index.php中引入它,(说明下,以下示例都需要引入这个autoloader.php文件,我们默认省略),下面就可以直接开发了。

require_once 'src/autoloader.php';

创建Editors

1、createEditor

grafika通过静态方法createEditor来创建一个editor。它包含所有的图片处理方法。

由于,grafika是基于ImagickGD库,所以使用createEditor方法会根据当前情况,自动选择所需要的图片处理库。(推荐使用)

use Grafika\Grafika; // Import package
$editor = Grafika::createEditor(); // Create the best available editor

2、Imagick Editor

当然你也可以直接使用Imagick类库。

use Grafika\Imagick\Editor; // Import package
$editor = new Editor(); // Imagick editor

注意:有些情况可能不支持该类库,你需要使用下面语句检查后使用,(不过你最好直接使用方法1,就没这些事)

use Grafika\Imagick\Editor; // Import package
$editor = new Editor(); // Imagick editor
if( $editor->isAvailable() ) { // Safety check

    // Your code here

}

3、GD Editor

你也可以直接使用GD库,也有些情况可能不支持,记得检查

use Grafika\Gd\Editor; // Import package
$editor = new Editor(); // Gd editor
if( $editor->isAvailable() ) { // Safety check

    // Your code here

}

创建图像

grafika允许你使用4种方式创建一个待处理的图像

1、直接打开图像

创建editor + open方法

use Grafika\Grafika;
$editor = Grafika::createEditor();
$editor->open( $image, 'path/to/image.jpg');

2、使用静态方法打开图片

使用直接打开、创建图片

use Grafika\Grafika;
$image = Grafika::createImage('path/to/image.jpg');

// 这里省略了$editor = Grafika::createEditor();

3、创建一个空白的画布

新建一个画布作为新图像

use Grafika\Grafika;
$image = Grafika::createBlankImage(100,100);

4、从已有图片拷贝一个

拷贝一个图像作为图像处理

$copy = clone $image;

这种方法你要保证之前有一张图片

这几种方法之后的操作大同小异,我们只选择第一种常规方法作为讲解示例

图片缩略图

我们先准备一个原图

图片描述

接下来,假设我们要创建的缩略图长:200px宽200px

1、Resize Fit

等比例缩放类型。那么就保证图片较长的一边不超过200px,等比缩放,缩放后不填充背景

use Grafika\Grafika;
$editor = Grafika::createEditor();
$editor->open($image1 , 'yanying.jpg'); // 打开yanying.jpg并且存放到$image1
$editor->resizeFit($image1 , 200 , 200);
$editor->save($image1 , 'yanying1.jpg');

$editor->open($image2 , 'yanying-h.jpg'); // 打开yanying.jpg并且存放到$image2
$editor->resizeFit($image2 , 200 , 200);
$editor->save($image2 , 'yanying2.jpg');

当然不要忘了第一行的require

图片描述图片描述

2、Resize Exact

固定尺寸缩放类型。就是不管图片长宽比,全部缩小到200px,可能导致图片变形。

use Grafika\Grafika;
$editor = Grafika::createEditor();
$editor->open($image1 , 'yanying.jpg'); // 打开yanying.jpg并且存放到$image1
$editor->resizeExact($image1 , 200 , 200);
$editor->save($image1 , 'yanying1.jpg');

$editor->open($image2 , 'yanying-h.jpg'); // 打开yanying.jpg并且存放到$image2
$editor->resizeExact($image2 , 200 , 200);
$editor->save($image2 , 'yanying2.jpg');

图片描述图片描述

3、Resize Fill

居中剪裁。就是把较短的变缩放到200px,然后将长边的大于200px的部分居中剪裁掉,图片不会变形。

use Grafika\Grafika;
$editor = Grafika::createEditor();
$editor->open($image1 , 'yanying.jpg'); // 打开yanying.jpg并且存放到$image1
$editor->resizeFill($image1 , 200,200);
$editor->save($image1 , 'yanying1.jpg');

$editor->open($image2 , 'yanying-h.jpg'); // 打开yanying.jpg并且存放到$image2
$editor->resizeFill($image2 , 200,200);
$editor->save($image2 , 'yanying2.jpg');

图片描述图片描述

4、Resize Exact Width

等宽缩放。和第一种功能相似,最终宽为200px,等比缩放,高度不管。

use Grafika\Grafika;
$editor = Grafika::createEditor();
$editor->open($image1 , 'yanying.jpg'); // 打开yanying.jpg并且存放到$image1
$editor->resizeExactWidth($image1 , 200);
$editor->save($image1 , 'yanying1.jpg');

$editor->open($image2 , 'yanying-h.jpg'); // 打开yanying.jpg并且存放到$image2
$editor->resizeExactWidth($image2 , 200);
$editor->save($image2 , 'yanying2.jpg');

图片描述

图片描述

5、Resize Exact Height

等高缩放。最终高为200px,等比缩放,不考虑图片宽度。

图片描述图片描述

图像对比功能

1、图片相似度对比

我们首先准备一张基本图,用来和其他图片对比。(segmentfault网页图片可能处理过,直接使用本文图片可能结果不一致)

图片描述

1、我们第一次使用一张灰度图片来比较

图片描述

use Grafika\Grafika;
$editor = Grafika::createEditor();
$result = $editor->compare('yanying.jpg' , 'yanying_grey.jpg');
var_dump($result); // int 2

说明: grafika图片对比方法compare返回一个数字,其中如果数字越接近于0,那么表示图片越相似。如果数字在0-10范围内,那么图片都可能相似。但是如果数字大于10,那么,可能就完全不同。

这里返回2,说明相似度还是非常高的。

2、我们再用一张缩小的图片来测试,记住都是和第一张基本图比较。

图片描述

use Grafika\Grafika;
$editor = Grafika::createEditor();
$result = $editor->compare('yanying.jpg' , 'yanying-smaller.jpg');
var_dump($result); // int 0

这里结果返回0,相似度非常高。

3、我们再用一张剪裁下来的局部图片测试

图片描述

use Grafika\Grafika;
$editor = Grafika::createEditor();
$result = $editor->compare('yanying.jpg' , 'yanying-half.jpg');
var_dump($result); // int 20

结果超过10了,相似度不怎么高

4、我们再用一张完全不同的图片测试

图片描述

use Grafika\Grafika;
$editor = Grafika::createEditor();
$result = $editor->compare('yanying.jpg' , 'yanying-h.jpg');
var_dump($result); // int 39

结果39,越来越大,越来越不像

2、比较图片是否相同

grafika提供方法equal来检查两张图片是否完全相同。这里的检查是一个像素一个像素的检测,所以时间可能会较长。

当然grafika也会预检查,如果两张图片大小不相同,则直接返回false。只有其他都相同后才会进行逐像素检查。

我们这里对比之前创建的一张缩略图,因为大小不一致,所以直接返回false

图片描述

use Grafika\Grafika;
$editor = Grafika::createEditor();
$result = $editor->equal('yanying.jpg' , 'yanying-smaller.jpg');
var_dump($result); // boolean false

智能剪裁

智能剪裁是自动识别图像中的重要部分,剪裁时候偏向于保留重点部分。

不过grafika也提供了人为操控位置剪裁,我们先说这个。

基本位置剪裁

基本位置剪裁包含9个位置

  • top-left
  • top-center
  • top-right
  • center-left
  • center
  • center-right
  • bottom-left
  • bottom-center
  • bottom-right

我们这里一起说了,这里我们使用900*600的图片,分成9块

图片描述

use Grafika\Grafika;
$editor = Grafika::createEditor();

$src = 'yanying.jpg';
$editor->open( $image, $src );
$editor->crop( $image, 300, 200, 'top-left' );
$editor->save( $image, 'result1.jpg' );
$editor->free( $image );

$editor->open( $image, $src );
$editor->crop( $image, 300, 200, 'top-center' );
$editor->save( $image, 'result2.jpg' );
$editor->free( $image );

$editor->open( $image, $src );
$editor->crop( $image, 300, 200, 'top-right' );
$editor->save( $image, 'result3.jpg' );
$editor->free( $image );

$editor->open( $image, $src );
$editor->crop( $image, 300, 200, 'center-left' );
$editor->save( $image, 'result4.jpg' );
$editor->free( $image );

$editor->open( $image, $src );
$editor->crop( $image, 300, 200, 'center' );
$editor->save( $image, 'result5.jpg' );
$editor->free( $image );

$editor->open( $image, $src );
$editor->crop( $image, 300, 200, 'center-right' );
$editor->save( $image, 'result6.jpg' );
$editor->free( $image );

$editor->open( $image, $src );
$editor->crop( $image, 300, 200, 'bottom-left' );
$editor->save( $image, 'result7.jpg' );
$editor->free( $image );

$editor->open( $image, $src );
$editor->crop( $image, 300, 200, 'bottom-center' );
$editor->save( $image, 'result8.jpg' );
$editor->free( $image );

$editor->open( $image, $src );
$editor->crop( $image, 300, 200, 'bottom-right' );
$editor->save( $image, 'result9.jpg' );
$editor->free( $image );

看下结果

图片描述

智能剪裁

原图

图片描述

我们使用智能剪裁将图片剪裁至200*200px

use Grafika\Grafika;
$editor = Grafika::createEditor();
$editor->open( $image, 'yanying-smaller.jpg' );
$editor->crop( $image, 200, 200, 'smart' );
$editor->save( $image, 'yanying-smart.jpg' );

发现还是可以突出重点的

图片描述

GIF缩略图

压缩GIF,不丢失动画

grafika可以直接压缩GIF图片,并且不丢失动画功能。

图片描述

use Grafika\Grafika;
$editor = Grafika::createEditor();
$editor->open( $image, 'sample.gif' );
$editor->resizeFit( $image, 250, 128 );
$editor->save( $image, 'output.gif' );

我们这里将原图压缩到原来的一半,发现动画并没有丢失

图片描述

移除GIF动画效果

当然,如果有需要,我们也可以直接移除GIF的动画效果

use Grafika\Grafika;
$editor = Grafika::createEditor();
$editor->open( $image, 'sample.gif' );
$editor->flatten( $image );
$editor->save( $image, 'output-no-animation.gif' );

图片描述

图片合并

图片合并需要2张图片,将其中一张作为基本图,准备的第二章图片就是放置在基础图片之上。

我们首先来看代码

use Grafika\Grafika;
$editor = Grafika::createEditor();
$editor->open($image1 , 'yanying-h.jpg');
$editor->open($image2 , 'yanying-smaller.jpg');
$editor->blend ( $image1, $image2 , 'normal', 0.9, 'center');
$editor->save($image1,'333/yanying-blend.jpg');

解释一下

首先打开两张图片,其中$image1为基础图片,也就是放在下面的。重点在blend这个方法。

其中

  • 第一个参数为基础图片
  • 第二个参数为放置在基础图片之上的图片normal, multiply, overlay or screen.,这里的类型意思就是图片叠加的模式,下面会给出实例看每种的不同。
  • 第三个参数为透明度,这个不说太多,容易想到。
  • 第四个为位置,有10个选择,其中,前面9种为用户自定义拜访位置,而最后一个是智能拜访,由grafika来判断摆放在哪里好。 top-left, top-center, top-right, center-left, center, center-right, bottom-left, bottom-center, bottom-right and smart
  • 第五个参数为可选参数,表示图片2距离图片1左边的距离
  • 第六个参数也为可选参数,表示图片2距离图片1上边的距离

我们试着摆几种情况。

1、normal

其中位置信息:center,透明度为0.9,也就是上面代码的那种

图片描述

2、multiply

位置信息:,top-left,其他不变

图片描述

3、overlay

位置信息:bottom-right,其他不变

图片描述

4、screen

位置信息:,最后一个位置参数不给,也就是默认top-left

图片描述

图像旋转

图像旋转比较简单,只需要给一个旋转角度参数就可以了,如果想要给背景填充个颜色,再给一个颜色参数即可。(默认不给背景色为黑色)

代码如下

use Grafika\Grafika;
use Grafika\Color;
$editor = Grafika::createEditor();
$editor->open($image , 'yanying-smaller.jpg');
$editor->rotate($image ,'45',new Color('#ff0000'));
$editor->save($image,'333/yanying-rotate.jpg');

最后一个背景颜色参数也是需要Color对象

图片描述

图片写文字

在图片上面写文字的参数比较多,不过如果正常使用,只需要给前两个必填的即可,后面的参数都是可选的。

我们逐一的来看各个参数

  • image:所需要写文字的图片
  • text:需要写的文字
  • size:(选填)字体大小,默认为12px
  • x:(选填)文字的最左边距离图片最左边的距离,默认为0
  • y:(选填)文字的基线到图片的最上边的距离,默认是12px,也就是文字的高度。(基线你就当做文字最下面好了)
  • color:(选填)字体颜色,Color对象,需要new Color一下,默认为黑色。
  • font:(选填)字体的完整路径,默认Sans font.
  • angle:(选填)文字旋转角度,取值范围为0-359,默认为0,也就是不旋转

我们随便找个文字试试

use Grafika\Grafika;
use Grafika\Color;
$editor = Grafika::createEditor();
$editor->open($image , 'yanying-smaller.jpg');
$editor->text($image ,'yanying',30,200,100,new Color("#000000"),'',45);
$editor->save($image,'333/yanying-text.jpg');

看下效果。这里说明下,如果文字为中文,需要找一个支持中文的字体。默认字体不支持中文,所以你写中文,就是都是小方框。

图片描述


严颖,PHP研发工程师

2016-11-07日晚

博客:segmentfault主页

推荐一个我们团队自己开发的针对开发者的网址导航:笔点导航 - 用心做最简洁的网址导航

可以自定义网址
可以自定义分类
分类可以标记颜色
自定义皮肤
自定义搜索
网址拖拽排序
自定义插件小模块

图片描述

查看原文

shinn_lancelot 收藏了文章 · 2019-05-29

Canvas 在高清屏下绘制图片变模糊的解决方法

之前我在 SF 上回答过「html5 canvas绘制图片模糊的问题」,但是可能是由于我给出的答案过于简略,加上答案中的 demo 链接已经失效,很多人反映这种办法并不好使。但是我在给出答案之前是在小米2 和 iPhone 上测试过的,没有任何问题。下面我会一步一步地描述具体的步骤。

前提条件

假设我们要在 canvas 中绘制一张 300 x 90 的图片,并且要保证它在高清屏中不模糊。那么我们首先要准备一张 600 x 180 的图片,处理过高清屏的同学应该会有这方面的经验。

问题重现

OK,我们先把问题重现一下,以便有一个更直观的了解。下面是相关的代码(为了简单起见,我把代码都写在了 HTML 文档里面):

<!-- 通过 img 标签引入图片,以便绘制到 canvas 中 -->
<img data-original="html5rocks.png" alt="" width="300" height="90">

<!-- canvas -->
<canvas width="300" height="90"></canvas>

<script>
    function init() {
        var canvas = document.querySelector('canvas');
        var ctx = canvas.getContext('2d');
        ctx.drawImage(document.querySelector('img'), 0, 0, 300, 90);
    }
    window.onload = init;
</script>

代码很简单,没有做任何处理,具体的效果和完整的代码可以查看这个 DEMO,这个 demo 在高清屏的手机中会出现的问题:canvas 中的图片变模糊了!。

至于为什么会变模糊,这和浏览器处理 canvas 的方式有关,相关的文章可以参考这篇 High DPI Canvas,这里不作深入介绍。

解决问题

首先,引入 hidpi-canvas-polyfill

其实,不只是绘制图片时会出现模糊的问题,正常情况下,在高清屏的设备中,任何绘制在 canvas 中的图形(包括文字)都会出现模糊的问题。上面这个 polyfill 就是为了解决这个问题,但是它没有处理图片。

接下来,修改绘制图片的代码

init 函数修改成下面这样:

function init() {
    var canvas = document.querySelector('canvas');
    var ctx = canvas.getContext('2d');

    // polyfill 提供了这个方法用来获取设备的 pixel ratio
    var getPixelRatio = function(context) {
        var backingStore = context.backingStorePixelRatio ||
            context.webkitBackingStorePixelRatio ||
            context.mozBackingStorePixelRatio ||
            context.msBackingStorePixelRatio ||
            context.oBackingStorePixelRatio ||
            context.backingStorePixelRatio || 1;
    
        return (window.devicePixelRatio || 1) / backingStore;
    };

    var ratio = getPixelRatio(ctx);
    
    // 注意,这里的 width 和 height 变成了 width * ratio 和 height * ratio
    ctx.drawImage(document.querySelector('img'), 0, 0, 300 * ratio, 90 * ratio);
}

可以点击这里查看完整的代码和效果,在高清屏的设备中打开,看看 cavans 中的图片是否完美显示。

说明

这个解决方案本质上和 @白一梓 的答案是一样的:先放大 canvas,再用 CSS 将其限制回原始大小。

如果你看了 polyfill 的代码就会明白其中的原理了,这个 polyfill 的代码十分简短明了,它做了两件事:一是将 canvas 的高和宽分别乘以 ratio 将其放大,然后又用 CSS 将高和宽限制成初始的大小;二是 hack canvas 中常用的函数,如:fillRect, clearRect, lineTo, arc 等,将它们的参数都乘以 ratio,以方便我们可以像以前那样使用这些方法,而不用在传参的时候手动乘以 ratio。

查看原文

shinn_lancelot 赞了文章 · 2019-04-09

电视机顶盒web开发总结,避免踩坑。

1.电视机顶盒web开发总结

针对东方有线机顶盒UUTVOS操作系统中内置的联彤浏览器web开发,总结一些自己在开发中遇到的问题和技巧。浏览器是基于Firefox的阉割版,所以开发中有一些莫名其妙的坑。已经尝试过使用Vue开发机顶盒web项目,体验较差:首次加载时间长、页面卡顿。由于项目进度推进,当时没有尝试组件懒加载和路由懒加载处理,这样做或许可以减少首次加载时间。推荐使用 JQuery 进行开发。

1.1采坑预告

1.2开发总结

1.2.1一个WebStrom就够了☞↑

我们的后台是现成的,直接把代码拷贝到服务器上,在机顶盒上就可以随时预览到项目。
  • WebStrom 的工具栏中的 Tools>Deployment 可以连接到配置远程服务器上,每次 CTRL + S 会自动上传项目文件,好用的不要不要的。墙裂推荐!
  • 通过配置 WebStrom,可以监听编译 Sass 文件,CTRL + S 自动编译就是这么方便。
  • 喜欢 VSCode 的话,未尝不可,或许 VSCode 里也有这些功能插件,我没去折腾罢了。
  • 如果在 WS 中使用了 Sass 或者 Less ,每次保存的时候,被编译后的 CSS 文件是不会自动上传到服务器上的,需要在 WS 里手动上传。

1.2.2用自己喜欢的技术☞↑

  • 机顶盒web开发官方文档推荐用原生 JS 开发,目前来看的话,JQ 用起来方便一些,暂时没有性能缺陷。
  • Less、Sass 两个都大爱。变量的威力大大的,就算美工切得是1080p机器的图,我拿来布局到720p上,利用 Sass 的变量和计算特性,非常容易控制CSS中的属性值。
  • 做列表渲染的时候用到了 art-template,腾讯出的一个模板引擎,参考它的文档,还是很容易上手的。官方文档

1.2.3少用JS控制呈现 HTML 元素☞↑

机顶盒浏览器的性能非常低,如果还要做视频播放的话,JS 可发挥的空间相当有限。
  • 一个 Tab 栏下有6个选项,选项里面 HTML 结构基本都是相同的,如果你打算用 JS 复用相同结构的 HTML 代码的话,赶紧停下,像我图片上这样老老实实的 copy 和 paste HTML代码吧。不然切换 Tab 的时候,随机的卡顿很恶心。
    clipboard.png
  • 类似 $(id).css({"backgroundImage":"url('...')"})$(id).attr({"src":"./*.jpg"}) 这样的在 JS 里面控制 UI 显示层面的操作要避免,尽量直接在 HTML 中完成,最多能接受这个操作: $(id).addClass()。机顶盒浏览器就是这么傲娇。(这是我试出来的,至于JQ操作性能方面的差异本质还是需要研究的。)
  • 机顶盒web中按钮的尺寸一般都很大,按钮背景图这些东西,就不要在 JS 中去操作,如果播放视频引起了性能高损耗,这个时候web中的UI卡的你一愣一愣的。

1.2.4“焦点事件”使用一时爽☞↑

  • 一定要避免使用"焦点事件"触发相关操作,焦点事件是高频率的系统事件,web在机顶盒运行时,焦点事件一般不受开发人员的绝对控制。“失去焦点”事件同样要避免使用。
  • “焦点事件”与“上下左右按键事件”具有一定的耦合性,“焦点事件”使用不当,问题百出。
  • 上下左右按键事件,一般都可以替换焦点事件。
  • a:focus {} 这个CSS选择器可以放心的使用。

1.2.5万能的 setTimeout() ☞↑

机顶盒内置的浏览器很恶心啊,阉割版的就算了,一些逻辑上的东西跟PC上也不同。
  • 一些操作无论怎么写都不运行,或者拿不到值(null),特别是在页面加载、父子页面跳转这些场景下。给它加个 setTimeout(function(), ms) 就搞定了,百试百灵,一般人我都不告诉他^_^。
$(document).keydown(function () {
    if (event.which  === 4097) {
        var distance = $("#list").scrollTop();
        sessionStorage.removeItem("listScrollTopVal");
        sessionStorage.setItem("listScrollTopVal", JSON.stringify(distance));
        // 按下确定键后,把获得焦点的元素的 id 保存到 sessionStorage中,
        // 这个时候就要在外边加一个延时函数,甚至可以将时间设置成 0ms 也行。
        setTimeout(function () {
            sessionStorage.removeItem("listFocusItemId");
            sessionStorage.setItem("listFocusItemId",JSON.stringify(document.activeElement.id));
        }, 100);
    }
});

1.2.6绝对定位position:absolte;省时省力☞↑

  • 机顶盒的可视区域是固定的,绝对定位是最省时省力的。
  • 拥有绝对定位元素的父元素必须是 position:relative定位,这个是必须的!
  • 多个块级元素排列在同一行,考虑使用display:inline-block;,优于使用flaot:...浮动布局。

1.2.7overflow:scroll;不能往上滚动☞↑

电视机的可视区域固定,整个页面是不滚动的,业务场景中,页面中的局部需要滚动:列表页、详情页。
  • 在PC上,给需要滚动的元素设置:overflow:scroll; 会出现滚动条,实现滚动。但是在电视机顶盒上,出现了:能往下滚动,不能往上滚动的问题。
  • 解决办法:给需要滚动的元素包裹一个 <a href="#"></a>并且必须设置display:block。;
<div class="content">
    <a href="#/" style="display:block;outline:none;">
      <div class="content-html">需要滚动的内容</div>
    </a>
  </div>

1.2.8切换视频播放,加防抖必不可少☞↑

机顶盒浏览器的性能本来就很差,在同一个页面的 Tab 上切换多个视频播放,按键过快的情况下,UI上焦点连续切换过去很多个元素了,视频的播放地址才挨个往过去切换,这个时候很容易造成卡顿或者浏览器假死。
  • 防抖其实就是一个延时函数,可以想象成:刷卡上公交车,只要有人刷卡,司机就不能开车。
$("#nav--second").keydown(function(event){
    if(event.which === 39) {
        // 这里的EVAN是一个全局的命名空间,EVAN.timer是一个全局变量
        clearTimeout(EVAN.timer);
        EVAN.timer = setTimeout(function () 
        create(EVAN.homePageVideoUrlArr[2]);
        }, EVAN.gap);  // 时间1-2s左右比较合适。
    }
});
查看原文

赞 12 收藏 9 评论 8

shinn_lancelot 收藏了文章 · 2019-02-03

入门 Webpack,看这篇就够了

2018年8月25日更新,目前 webpack 已经更新值 4.17.1 ,本文所用到的各种库或多或少有些过时,跟着代码操作下来可能会遇到各种问题,不过 webpack 的主体思想没变,所以还是希望本文对新学 webpack 的你,有所帮助。此外用基于 webpack 4.17.1 写了一个简单的demo,如果遇到啥问题,可以参考,之后应该会逐步来完善这个demo,如果有啥通用的想实现的功能,也可以在里面提 issue。

2017年12月7日更新,添加了clean-webpack-plugin,babel-env-preset,添加本文涉及到的所有代码的示例,如果你在学习过程中出错了,可点击此处参考(有些过时了,不要再 fork 了)

写在前面的话

阅读本文之前,先看下面这个webpack的配置文件,如果每一项你都懂,那本文能带给你的收获也许就比较有限,你可以快速浏览或直接跳过;如果你和十天前的我一样,对很多选项存在着疑惑,那花一段时间慢慢阅读本文,你的疑惑一定一个一个都会消失;如果你以前没怎么接触过Webpack,而你又你对webpack感兴趣,那么动手跟着本文中那个贯穿始终的例子写一次,写完以后你会发现你已明明白白的走进了Webpack的大门。
// 一个常见的`webpack`配置文件
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const ExtractTextPlugin = require('extract-text-webpack-plugin');

module.exports = {
        entry: __dirname + "/app/main.js", //已多次提及的唯一入口文件
        output: {
            path: __dirname + "/build",
            filename: "bundle-[hash].js"
        },
        devtool: 'none',
        devServer: {
            contentBase: "./public", //本地服务器所加载的页面所在的目录
            historyApiFallback: true, //不跳转
            inline: true,
            hot: true
        },
        module: {
            rules: [{
                    test: /(\.jsx|\.js)$/,
                    use: {
                        loader: "babel-loader"
                    },
                    exclude: /node_modules/
                }, {
                    test: /\.css$/,
                    use: ExtractTextPlugin.extract({
                        fallback: "style-loader",
                        use: [{
                            loader: "css-loader",
                            options: {
                                modules: true,
                                localIdentName: '[name]__[local]--[hash:base64:5]'
                            }
                        }, {
                            loader: "postcss-loader"
                        }],
                    })
                }
            }
        ]
    },
    plugins: [
        new webpack.BannerPlugin('版权所有,翻版必究'),
        new HtmlWebpackPlugin({
            template: __dirname + "/app/index.tmpl.html" //new 一个这个插件的实例,并传入相关的参数
        }),
        new webpack.optimize.OccurrenceOrderPlugin(),
        new webpack.optimize.UglifyJsPlugin(),
        new ExtractTextPlugin("style.css")
    ]
};

什么是WebPack,为什么要使用它?

为什要使用WebPack

现今的很多网页其实可以看做是功能丰富的应用,它们拥有着复杂的JavaScript代码和一大堆依赖包。为了简化开发的复杂度,前端社区涌现出了很多好的实践方法

  • 模块化,让我们可以把复杂的程序细化为小的文件;
  • 类似于TypeScript这种在JavaScript基础上拓展的开发语言:使我们能够实现目前版本的JavaScript不能直接使用的特性,并且之后还能转换为JavaScript文件使浏览器可以识别;
  • Scss,less等CSS预处理器
  • ...

这些改进确实大大的提高了我们的开发效率,但是利用它们开发的文件往往需要进行额外的处理才能让浏览器识别,而手动处理又是非常繁琐的,这就为WebPack类的工具的出现提供了需求。

什么是Webpack

WebPack可以看做是模块打包机:它做的事情是,分析你的项目结构,找到JavaScript模块以及其它的一些浏览器不能直接运行的拓展语言(Scss,TypeScript等),并将其转换和打包为合适的格式供浏览器使用。

WebPack和Grunt以及Gulp相比有什么特性

其实Webpack和另外两个并没有太多的可比性,Gulp/Grunt是一种能够优化前端的开发流程的工具,而WebPack是一种模块化的解决方案,不过Webpack的优点使得Webpack在很多场景下可以替代Gulp/Grunt类的工具。

Grunt和Gulp的工作方式是:在一个配置文件中,指明对某些文件进行类似编译,组合,压缩等任务的具体步骤,工具之后可以自动替你完成这些任务。
Grunt和Gulp的工作流程

Webpack的工作方式是:把你的项目当做一个整体,通过一个给定的主文件(如:index.js),Webpack将从这个文件开始找到你的项目的所有依赖文件,使用loaders处理它们,最后打包为一个(或多个)浏览器可识别的JavaScript文件。
Webpack工作方式

如果实在要把二者进行比较,Webpack的处理速度更快更直接,能打包更多不同类型的文件。

开始使用Webpack

初步了解了Webpack工作方式后,我们一步步的开始学习使用Webpack。

安装

Webpack可以使用npm安装,新建一个空的练习文件夹(此处命名为webpack sample project),在终端中转到该文件夹后执行下述指令就可以完成安装。

//全局安装
npm install -g webpack
//安装到你的项目目录
npm install --save-dev webpack

正式使用Webpack前的准备

  1. 在上述练习文件夹中创建一个package.json文件,这是一个标准的npm说明文件,里面蕴含了丰富的信息,包括当前项目的依赖模块,自定义的脚本任务等等。在终端中使用npm init命令可以自动创建这个package.json文件
npm init

输入这个命令后,终端会问你一系列诸如项目名称,项目描述,作者等信息,不过不用担心,如果你不准备在npm中发布你的模块,这些问题的答案都不重要,回车默认即可。

  1. package.json文件已经就绪,我们在本项目中安装Webpack作为依赖包
// 安装Webpack
npm install --save-dev webpack
  1. 回到之前的空文件夹,并在里面创建两个文件夹,app文件夹和public文件夹,app文件夹用来存放原始数据和我们将写的JavaScript模块,public文件夹用来存放之后供浏览器读取的文件(包括使用webpack打包生成的js文件以及一个index.html文件)。接下来我们再创建三个文件:
  • index.html --放在public文件夹中;
  • Greeter.js-- 放在app文件夹中;
  • main.js-- 放在app文件夹中;

此时项目结构如下图所示
项目结构

我们在index.html文件中写入最基础的html代码,它在这里目的在于引入打包后的js文件(这里我们先把之后打包后的js文件命名为bundle.js,之后我们还会详细讲述)。

<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>Webpack Sample Project</title>
  </head>
  <body>
    <div id='root'>
    </div>
    <script data-original="bundle.js"></script>
  </body>
</html>

我们在Greeter.js中定义一个返回包含问候信息的html元素的函数,并依据CommonJS规范导出这个函数为一个模块:

// Greeter.js
module.exports = function() {
  var greet = document.createElement('div');
  greet.textContent = "Hi there and greetings!";
  return greet;
};

main.js文件中我们写入下述代码,用以把Greeter模块返回的节点插入页面。

//main.js 
const greeter = require('./Greeter.js');
document.querySelector("#root").appendChild(greeter());

正式使用Webpack

webpack可以在终端中使用,在基本的使用方法如下:

# {extry file}出填写入口文件的路径,本文中就是上述main.js的路径,
# {destination for bundled file}处填写打包文件的存放路径
# 填写路径的时候不用添加{}
webpack {entry file} {destination for bundled file}

指定入口文件后,webpack将自动识别项目所依赖的其它文件,不过需要注意的是如果你的webpack不是全局安装的,那么当你在终端中使用此命令时,需要额外指定其在node_modules中的地址,继续上面的例子,在终端中输入如下命令

# webpack非全局安装的情况
node_modules/.bin/webpack app/main.js public/bundle.js

结果如下

使用命令行打包

可以看出webpack同时编译了main.jsGreeter,js,现在打开index.html,可以看到如下结果
htmlResult1

有没有很激动,已经成功的使用Webpack打包了一个文件了。不过在终端中进行复杂的操作,其实是不太方便且容易出错的,接下来看看Webpack的另一种更常见的使用方法。

通过配置文件来使用Webpack

Webpack拥有很多其它的比较高级的功能(比如说本文后面会介绍的loadersplugins),这些功能其实都可以通过命令行模式实现,但是正如前面提到的,这样不太方便且容易出错的,更好的办法是定义一个配置文件,这个配置文件其实也是一个简单的JavaScript模块,我们可以把所有的与打包相关的信息放在里面。

继续上面的例子来说明如何写这个配置文件,在当前练习文件夹的根目录下新建一个名为webpack.config.js的文件,我们在其中写入如下所示的简单配置代码,目前的配置主要涉及到的内容是入口文件路径和打包后文件的存放路径。

module.exports = {
  entry:  __dirname + "/app/main.js",//已多次提及的唯一入口文件
  output: {
    path: __dirname + "/public",//打包后的文件存放的地方
    filename: "bundle.js"//打包后输出文件的文件名
  }
}
:“__dirname”是node.js中的一个全局变量,它指向当前执行脚本所在的目录。

有了这个配置之后,再打包文件,只需在终端里运行webpack(非全局安装需使用node_modules/.bin/webpack)命令就可以了,这条命令会自动引用webpack.config.js文件中的配置选项,示例如下:

配合配置文件进行打包

又学会了一种使用Webpack的方法,这种方法不用管那烦人的命令行参数,有没有感觉很爽。如果我们可以连webpack(非全局安装需使用node_modules/.bin/webpack)这条命令都可以不用,那种感觉会不会更爽~,继续看下文。

更快捷的执行打包任务

在命令行中输入命令需要代码类似于node_modules/.bin/webpack这样的路径其实是比较烦人的,不过值得庆幸的是npm可以引导任务执行,对npm进行配置后可以在命令行中使用简单的npm start命令来替代上面略微繁琐的命令。在package.json中对scripts对象进行相关设置即可,设置方法如下。

{
  "name": "webpack-sample-project",
  "version": "1.0.0",
  "description": "Sample webpack project",
  "scripts": {
    "start": "webpack" // 修改的是这里,JSON文件不支持注释,引用时请清除
  },
  "author": "zhang",
  "license": "ISC",
  "devDependencies": {
    "webpack": "3.10.0"
  }
}
注:package.json中的script会安装一定顺序寻找命令对应位置,本地的node_modules/.bin路径就在这个寻找清单中,所以无论是全局还是局部安装的Webpack,你都不需要写前面那指明详细的路径了。

npm的start命令是一个特殊的脚本名称,其特殊性表现在,在命令行中使用npm start就可以执行其对于的命令,如果对应的此脚本名称不是start,想要在命令行中运行时,需要这样用npm run {script name}npm run build,我们在命令行中输入npm start试试,输出结果如下:

使用npm start 打包代码

现在只需要使用npm start就可以打包文件了,有没有觉得webpack也不过如此嘛,不过不要太小瞧webpack,要充分发挥其强大的功能我们需要修改配置文件的其它选项,一项项来看。

Webpack的强大功能

生成Source Maps(使调试更容易)

开发总是离不开调试,方便的调试能极大的提高开发效率,不过有时候通过打包后的文件,你是不容易找到出错了的地方,对应的你写的代码的位置的,Source Maps就是来帮我们解决这个问题的。

通过简单的配置,webpack就可以在打包时为我们生成的source maps,这为我们提供了一种对应编译文件和源文件的方法,使得编译后的代码可读性更高,也更容易调试。

webpack的配置文件中配置source maps,需要配置devtool,它有以下四种不同的配置选项,各具优缺点,描述如下:

devtool选项配置结果
source-map在一个单独的文件中产生一个完整且功能完全的文件。这个文件具有最好的source map,但是它会减慢打包速度;
cheap-module-source-map在一个单独的文件中生成一个不带列映射的map,不带列映射提高了打包速度,但是也使得浏览器开发者工具只能对应到具体的行,不能对应到具体的列(符号),会对调试造成不便;
eval-source-map使用eval打包源文件模块,在同一个文件中生成干净的完整的source map。这个选项可以在不影响构建速度的前提下生成完整的sourcemap,但是对打包后输出的JS文件的执行具有性能和安全的隐患。在开发阶段这是一个非常好的选项,在生产阶段则一定不要启用这个选项;
cheap-module-eval-source-map这是在打包文件时最快的生成source map的方法,生成的Source Map 会和打包后的JavaScript文件同行显示,没有列映射,和eval-source-map选项具有相似的缺点;

正如上表所述,上述选项由上到下打包速度越来越快,不过同时也具有越来越多的负面作用,较快的打包速度的后果就是对打包后的文件的的执行有一定影响。

对小到中型的项目中,eval-source-map是一个很好的选项,再次强调你只应该开发阶段使用它,我们继续对上文新建的webpack.config.js,进行如下配置:

module.exports = {
  devtool: 'eval-source-map',
  entry:  __dirname + "/app/main.js",
  output: {
    path: __dirname + "/public",
    filename: "bundle.js"
  }
}
cheap-module-eval-source-map方法构建速度更快,但是不利于调试,推荐在大型项目考虑时间成本时使用。

使用webpack构建本地服务器

想不想让你的浏览器监听你的代码的修改,并自动刷新显示修改后的结果,其实Webpack提供一个可选的本地开发服务器,这个本地服务器基于node.js构建,可以实现你想要的这些功能,不过它是一个单独的组件,在webpack中进行配置之前需要单独安装它作为项目依赖

npm install --save-dev webpack-dev-server

devserver作为webpack配置选项中的一项,以下是它的一些配置选项,更多配置可参考这里

devserver的配置选项功能描述
contentBase默认webpack-dev-server会为根文件夹提供本地服务器,如果想为另外一个目录下的文件提供本地服务器,应该在这里设置其所在目录(本例设置到“public"目录)
port设置默认监听端口,如果省略,默认为”8080“
inline设置为true,当源文件改变时会自动刷新页面
historyApiFallback在开发单页应用时非常有用,它依赖于HTML5 history API,如果设置为true,所有的跳转将指向index.html

把这些命令加到webpack的配置文件中,现在的配置文件webpack.config.js如下所示

module.exports = {
  devtool: 'eval-source-map',

  entry:  __dirname + "/app/main.js",
  output: {
    path: __dirname + "/public",
    filename: "bundle.js"
  },

  devServer: {
    contentBase: "./public",//本地服务器所加载的页面所在的目录
    historyApiFallback: true,//不跳转
    inline: true//实时刷新
  } 
}

package.json中的scripts对象中添加如下命令,用以开启本地服务器:

  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "webpack",
    "server": "webpack-dev-server --open"
  },

在终端中输入npm run server即可在本地的8080端口查看结果

开启本地服务器

Loaders

鼎鼎大名的Loaders登场了!

Loaderswebpack提供的最激动人心的功能之一了。通过使用不同的loaderwebpack有能力调用外部的脚本或工具,实现对不同格式的文件的处理,比如说分析转换scss为css,或者把下一代的JS文件(ES6,ES7)转换为现代浏览器兼容的JS文件,对React的开发而言,合适的Loaders可以把React的中用到的JSX文件转换为JS文件。

Loaders需要单独安装并且需要在webpack.config.js中的modules关键字下进行配置,Loaders的配置包括以下几方面:

  • test:一个用以匹配loaders所处理文件的拓展名的正则表达式(必须)
  • loader:loader的名称(必须)
  • include/exclude:手动添加必须处理的文件(文件夹)或屏蔽不需要处理的文件(文件夹)(可选);
  • query:为loaders提供额外的设置选项(可选)

不过在配置loader之前,我们把Greeter.js里的问候消息放在一个单独的JSON文件里,并通过合适的配置使Greeter.js可以读取该JSON文件的值,各文件修改后的代码如下:

在app文件夹中创建带有问候信息的JSON文件(命名为config.json)

{
  "greetText": "Hi there and greetings from JSON!"
}

更新后的Greeter.js

var config = require('./config.json');

module.exports = function() {
  var greet = document.createElement('div');
  greet.textContent = config.greetText;
  return greet;
};
由于webpack3.*/webpack2.*已经内置可处理JSON文件,这里我们无需再添加webpack1.*需要的json-loader。在看如何具体使用loader之前我们先看看Babel是什么?

Babel

Babel其实是一个编译JavaScript的平台,它可以编译代码帮你达到以下目的:

  • 让你能使用最新的JavaScript代码(ES6,ES7...),而不用管新标准是否被当前使用的浏览器完全支持;
  • 让你能使用基于JavaScript进行了拓展的语言,比如React的JSX;

Babel的安装与配置

Babel其实是几个模块化的包,其核心功能位于称为babel-core的npm包中,webpack可以把其不同的包整合在一起使用,对于每一个你需要的功能或拓展,你都需要安装单独的包(用得最多的是解析Es6的babel-env-preset包和解析JSX的babel-preset-react包)。

我们先来一次性安装这些依赖包

// npm一次性安装多个依赖模块,模块之间用空格隔开
npm install --save-dev babel-core babel-loader babel-preset-env babel-preset-react

webpack中配置Babel的方法如下:

module.exports = {
    entry: __dirname + "/app/main.js",//已多次提及的唯一入口文件
    output: {
        path: __dirname + "/public",//打包后的文件存放的地方
        filename: "bundle.js"//打包后输出文件的文件名
    },
    devtool: 'eval-source-map',
    devServer: {
        contentBase: "./public",//本地服务器所加载的页面所在的目录
        historyApiFallback: true,//不跳转
        inline: true//实时刷新
    },
    module: {
        rules: [
            {
                test: /(\.jsx|\.js)$/,
                use: {
                    loader: "babel-loader",
                    options: {
                        presets: [
                            "env", "react"
                        ]
                    }
                },
                exclude: /node_modules/
            }
        ]
    }
};

现在你的webpack的配置已经允许你使用ES6以及JSX的语法了。继续用上面的例子进行测试,不过这次我们会使用React,记得先安装 React 和 React-DOM

npm install --save react react-dom

接下来我们使用ES6的语法,更新Greeter.js并返回一个React组件

//Greeter,js
import React, {Component} from 'react'
import config from './config.json';

class Greeter extends Component{
  render() {
    return (
      <div>
        {config.greetText}
      </div>
    );
  }
}

export default Greeter

修改main.js如下,使用ES6的模块定义和渲染Greeter模块

// main.js
import React from 'react';
import {render} from 'react-dom';
import Greeter from './Greeter';

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

重新使用npm start打包,如果之前打开的本地服务器没有关闭,你应该可以在localhost:8080下看到与之前一样的内容,这说明reactes6被正常打包了。

localhost:8080

Babel的配置

Babel其实可以完全在 webpack.config.js 中进行配置,但是考虑到babel具有非常多的配置选项,在单一的webpack.config.js文件中进行配置往往使得这个文件显得太复杂,因此一些开发者支持把babel的配置选项放在一个单独的名为 ".babelrc" 的配置文件中。我们现在的babel的配置并不算复杂,不过之后我们会再加一些东西,因此现在我们就提取出相关部分,分两个配置文件进行配置(webpack会自动调用.babelrc里的babel配置选项),如下:

module.exports = {
    entry: __dirname + "/app/main.js",//已多次提及的唯一入口文件
    output: {
        path: __dirname + "/public",//打包后的文件存放的地方
        filename: "bundle.js"//打包后输出文件的文件名
    },
    devtool: 'eval-source-map',
    devServer: {
        contentBase: "./public",//本地服务器所加载的页面所在的目录
        historyApiFallback: true,//不跳转
        inline: true//实时刷新
    },
    module: {
        rules: [
            {
                test: /(\.jsx|\.js)$/,
                use: {
                    loader: "babel-loader"
                },
                exclude: /node_modules/
            }
        ]
    }
};
//.babelrc
{
  "presets": ["react", "env"]
}

到目前为止,我们已经知道了,对于模块,Webpack能提供非常强大的处理功能,那那些是模块呢。

一切皆模块

Webpack有一个不可不说的优点,它把所有的文件都都当做模块处理,JavaScript代码,CSS和fonts以及图片等等通过合适的loader都可以被处理。

CSS

webpack提供两个工具处理样式表,css-loaderstyle-loader,二者处理的任务不同,css-loader使你能够使用类似@importurl(...)的方法实现 require()的功能,style-loader将所有的计算后的样式加入页面中,二者组合在一起使你能够把样式表嵌入webpack打包后的JS文件中。

继续上面的例子

//安装
npm install --save-dev style-loader css-loader
//使用
module.exports = {

   ...
    module: {
        rules: [
            {
                test: /(\.jsx|\.js)$/,
                use: {
                    loader: "babel-loader"
                },
                exclude: /node_modules/
            },
            {
                test: /\.css$/,
                use: [
                    {
                        loader: "style-loader"
                    }, {
                        loader: "css-loader"
                    }
                ]
            }
        ]
    }
};
请注意这里对同一个文件引入多个loader的方法。

接下来,在app文件夹里创建一个名字为"main.css"的文件,对一些元素设置样式

/* main.css */
html {
  box-sizing: border-box;
  -ms-text-size-adjust: 100%;
  -webkit-text-size-adjust: 100%;
}

*, *:before, *:after {
  box-sizing: inherit;
}

body {
  margin: 0;
  font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
}

h1, h2, h3, h4, h5, h6, p, ul {
  margin: 0;
  padding: 0;
}

我们这里例子中用到的webpack只有单一的入口,其它的模块需要通过 import, require, url等与入口文件建立其关联,为了让webpack能找到”main.css“文件,我们把它导入”main.js “中,如下

//main.js
import React from 'react';
import {render} from 'react-dom';
import Greeter from './Greeter';

import './main.css';//使用require导入css文件

render(<Greeter />, document.getElementById('root'));
通常情况下,css会和js打包到同一个文件中,并不会打包为一个单独的css文件,不过通过合适的配置webpack也可以把css打包为单独的文件的。

上面的代码说明webpack是怎么把css当做模块看待的,咱们继续看一个更加真实的css模块实践。

CSS module

在过去的一些年里,JavaScript通过一些新的语言特性,更好的工具以及更好的实践方法(比如说模块化)发展得非常迅速。模块使得开发者把复杂的代码转化为小的,干净的,依赖声明明确的单元,配合优化工具,依赖管理和加载管理可以自动完成。

不过前端的另外一部分,CSS发展就相对慢一些,大多的样式表却依旧巨大且充满了全局类名,维护和修改都非常困难。

被称为CSS modules的技术意在把JS的模块化思想带入CSS中来,通过CSS模块,所有的类名,动画名默认都只作用于当前模块。Webpack对CSS模块化提供了非常好的支持,只需要在CSS loader中进行简单配置即可,然后就可以直接把CSS的类名传递到组件的代码中,这样做有效避免了全局污染。具体的代码如下

module.exports = {

    ...

    module: {
        rules: [
            {
                test: /(\.jsx|\.js)$/,
                use: {
                    loader: "babel-loader"
                },
                exclude: /node_modules/
            },
            {
                test: /\.css$/,
                use: [
                    {
                        loader: "style-loader"
                    }, {
                        loader: "css-loader",
                        options: {
                            modules: true, // 指定启用css modules
                            localIdentName: '[name]__[local]--[hash:base64:5]' // 指定css的类名格式
                        }
                    }
                ]
            }
        ]
    }
};

我们在app文件夹下创建一个Greeter.css文件来进行一下测试

/* Greeter.css */
.root {
  background-color: #eee;
  padding: 10px;
  border: 3px solid #ccc;
}

导入.root到Greeter.js中

import React, {Component} from 'react';
import config from './config.json';
import styles from './Greeter.css';//导入

class Greeter extends Component{
  render() {
    return (
      <div className={styles.root}> //使用cssModule添加类名的方法
        {config.greetText}
      </div>
    );
  }
}

export default Greeter

放心使用把,相同的类名也不会造成不同组件之间的污染。

应用了css module后的样式

CSS modules 也是一个很大的主题,有兴趣的话可以去其官方文档了解更多。

CSS预处理器

SassLess 之类的预处理器是对原生CSS的拓展,它们允许你使用类似于variables, nesting, mixins, inheritance等不存在于CSS中的特性来写CSS,CSS预处理器可以这些特殊类型的语句转化为浏览器可识别的CSS语句,

你现在可能都已经熟悉了,在webpack里使用相关loaders进行配置就可以使用了,以下是常用的CSS 处理loaders:

  • Less Loader
  • Sass Loader
  • Stylus Loader

不过其实也存在一个CSS的处理平台-PostCSS,它可以帮助你的CSS实现更多的功能,在其官方文档可了解更多相关知识。

举例来说如何使用PostCSS,我们使用PostCSS来为CSS代码自动添加适应不同浏览器的CSS前缀。

首先安装postcss-loaderautoprefixer(自动添加前缀的插件)

npm install --save-dev postcss-loader autoprefixer

接下来,在webpack配置文件中添加postcss-loader,在根目录新建postcss.config.js,并添加如下代码之后,重新使用npm start打包时,你写的css会自动根据Can i use里的数据添加不同前缀了。

//webpack.config.js
module.exports = {
    ...
    module: {
        rules: [
            {
                test: /(\.jsx|\.js)$/,
                use: {
                    loader: "babel-loader"
                },
                exclude: /node_modules/
            },
            {
                test: /\.css$/,
                use: [
                    {
                        loader: "style-loader"
                    }, {
                        loader: "css-loader",
                        options: {
                            modules: true
                        }
                    }, {
                        loader: "postcss-loader"
                    }
                ]
            }
        ]
    }
}
// postcss.config.js
module.exports = {
    plugins: [
        require('autoprefixer')
    ]
}

至此,本文已经谈论了处理JS的Babel和处理CSS的PostCSS的基本用法,它们其实也是两个单独的平台,配合webpack可以很好的发挥它们的作用。接下来介绍Webpack中另一个非常重要的功能-Plugins

插件(Plugins)

插件(Plugins)是用来拓展Webpack功能的,它们会在整个构建过程中生效,执行相关的任务。
Loaders和Plugins常常被弄混,但是他们其实是完全不同的东西,可以这么来说,loaders是在打包构建过程中用来处理源文件的(JSX,Scss,Less..),一次处理一个,插件并不直接操作单个文件,它直接对整个构建过程其作用。

Webpack有很多内置插件,同时也有很多第三方插件,可以让我们完成更加丰富的功能。

使用插件的方法

要使用某个插件,我们需要通过npm安装它,然后要做的就是在webpack配置中的plugins关键字部分添加该插件的一个实例(plugins是一个数组)继续上面的例子,我们添加了一个给打包后代码添加版权声明的插件

const webpack = require('webpack');

module.exports = {
...
    module: {
        rules: [
            {
                test: /(\.jsx|\.js)$/,
                use: {
                    loader: "babel-loader"
                },
                exclude: /node_modules/
            },
            {
                test: /\.css$/,
                use: [
                    {
                        loader: "style-loader"
                    }, {
                        loader: "css-loader",
                        options: {
                            modules: true
                        }
                    }, {
                        loader: "postcss-loader"
                    }
                ]
            }
        ]
    },
    plugins: [
        new webpack.BannerPlugin('版权所有,翻版必究')
    ],
};

通过这个插件,打包后的JS文件显示如下

版权所有,翻版必究

这就是webpack插件的基础用法了,下面给大家推荐几个常用的插件

HtmlWebpackPlugin

这个插件的作用是依据一个简单的index.html模板,生成一个自动引用你打包后的JS文件的新index.html。这在每次生成的js文件名称不同时非常有用(比如添加了hash值)。

安装

npm install --save-dev html-webpack-plugin

这个插件自动完成了我们之前手动做的一些事情,在正式使用之前需要对一直以来的项目结构做一些更改:

  1. 移除public文件夹,利用此插件,index.html文件会自动生成,此外CSS已经通过前面的操作打包到JS中了。
  2. 在app目录下,创建一个index.tmpl.html文件模板,这个模板包含title等必须元素,在编译过程中,插件会依据此模板生成最终的html页面,会自动添加所依赖的 css, js,favicon等文件,index.tmpl.html中的模板源代码如下:
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>Webpack Sample Project</title>
  </head>
  <body>
    <div id='root'>
    </div>
  </body>
</html>

3.更新webpack的配置文件,方法同上,新建一个build文件夹用来存放最终的输出文件

const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
    entry: __dirname + "/app/main.js",//已多次提及的唯一入口文件
    output: {
        path: __dirname + "/build",
        filename: "bundle.js"
    },
    devtool: 'eval-source-map',
    devServer: {
        contentBase: "./public",//本地服务器所加载的页面所在的目录
        historyApiFallback: true,//不跳转
        inline: true//实时刷新
    },
    module: {
        rules: [
            {
                test: /(\.jsx|\.js)$/,
                use: {
                    loader: "babel-loader"
                },
                exclude: /node_modules/
            },
            {
                test: /\.css$/,
                use: [
                    {
                        loader: "style-loader"
                    }, {
                        loader: "css-loader",
                        options: {
                            modules: true
                        }
                    }, {
                        loader: "postcss-loader"
                    }
                ]
            }
        ]
    },
    plugins: [
        new webpack.BannerPlugin('版权所有,翻版必究'),
        new HtmlWebpackPlugin({
            template: __dirname + "/app/index.tmpl.html"//new 一个这个插件的实例,并传入相关的参数
        })
    ],
};

再次执行npm start你会发现,build文件夹下面生成了bundle.jsindex.html

build文件夹

Hot Module Replacement

Hot Module Replacement(HMR)也是webpack里很有用的一个插件,它允许你在修改组件代码后,自动刷新实时预览修改后的效果。

在webpack中实现HMR也很简单,只需要做两项配置

  1. 在webpack配置文件中添加HMR插件;
  2. 在Webpack Dev Server中添加“hot”参数;

不过配置完这些后,JS模块其实还是不能自动热加载的,还需要在你的JS模块中执行一个Webpack提供的API才能实现热加载,虽然这个API不难使用,但是如果是React模块,使用我们已经熟悉的Babel可以更方便的实现功能热加载。

整理下我们的思路,具体实现方法如下

  • Babelwebpack是独立的工具
  • 二者可以一起工作
  • 二者都可以通过插件拓展功能
  • HMR是一个webpack插件,它让你能浏览器中实时观察模块修改后的效果,但是如果你想让它工作,需要对模块进行额外的配额;
  • Babel有一个叫做react-transform-hrm的插件,可以在不对React模块进行额外的配置的前提下让HMR正常工作;

还是继续上例来实际看看如何配置

const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
    entry: __dirname + "/app/main.js",//已多次提及的唯一入口文件
    output: {
        path: __dirname + "/build",
        filename: "bundle.js"
    },
    devtool: 'eval-source-map',
    devServer: {
        contentBase: "./public",//本地服务器所加载的页面所在的目录
        historyApiFallback: true,//不跳转
        inline: true,
        hot: true
    },
    module: {
        rules: [
            {
                test: /(\.jsx|\.js)$/,
                use: {
                    loader: "babel-loader"
                },
                exclude: /node_modules/
            },
            {
                test: /\.css$/,
                use: [
                    {
                        loader: "style-loader"
                    }, {
                        loader: "css-loader",
                        options: {
                            modules: true
                        }
                    }, {
                        loader: "postcss-loader"
                    }
                ]
            }
        ]
    },
    plugins: [
        new webpack.BannerPlugin('版权所有,翻版必究'),
        new HtmlWebpackPlugin({
            template: __dirname + "/app/index.tmpl.html"//new 一个这个插件的实例,并传入相关的参数
        }),
        new webpack.HotModuleReplacementPlugin()//热加载插件
    ],
};
   

安装react-transform-hmr

npm install --save-dev babel-plugin-react-transform react-transform-hmr

配置Babel

// .babelrc
{
  "presets": ["react", "env"],
  "env": {
    "development": {
    "plugins": [["react-transform", {
       "transforms": [{
         "transform": "react-transform-hmr",
         
         "imports": ["react"],
         
         "locals": ["module"]
       }]
     }]]
    }
  }
}

现在当你使用React时,可以热加载模块了,每次保存就能在浏览器上看到更新内容。

产品阶段的构建

目前为止,我们已经使用webpack构建了一个完整的开发环境。但是在产品阶段,可能还需要对打包的文件进行额外的处理,比如说优化,压缩,缓存以及分离CSS和JS。

对于复杂的项目来说,需要复杂的配置,这时候分解配置文件为多个小的文件可以使得事情井井有条,以上面的例子来说,我们创建一个webpack.production.config.js的文件,在里面加上基本的配置,它和原始的webpack.config.js很像,如下

// webpack.production.config.js
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
    entry: __dirname + "/app/main.js", //已多次提及的唯一入口文件
    output: {
        path: __dirname + "/build",
        filename: "bundle.js"
    },
    devtool: 'null', //注意修改了这里,这能大大压缩我们的打包代码
    devServer: {
        contentBase: "./public", //本地服务器所加载的页面所在的目录
        historyApiFallback: true, //不跳转
        inline: true,
        hot: true
    },
    module: {
        rules: [{
            test: /(\.jsx|\.js)$/,
            use: {
                loader: "babel-loader"
            },
            exclude: /node_modules/
        }, {
            test: /\.css$/,
            use: ExtractTextPlugin.extract({
                fallback: "style-loader",
                use: [{
                    loader: "css-loader",
                    options: {
                        modules: true
                    }
                }, {
                    loader: "postcss-loader"
                }],
            })
        }]
    },
    plugins: [
        new webpack.BannerPlugin('版权所有,翻版必究'),
        new HtmlWebpackPlugin({
            template: __dirname + "/app/index.tmpl.html" //new 一个这个插件的实例,并传入相关的参数
        }),
        new webpack.HotModuleReplacementPlugin() //热加载插件
    ],
};
//package.json
{
  "name": "test",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "webpack",
    "server": "webpack-dev-server --open",
    "build": "NODE_ENV=production webpack --config ./webpack.production.config.js --progress"
  },
  "author": "",
  "license": "ISC",
  "devDependencies": {
...
  },
  "dependencies": {
    "react": "^15.6.1",
    "react-dom": "^15.6.1"
  }
}
注意:如果是window电脑,build需要配置为"build": "set NODE_ENV=production && webpack --config ./webpack.production.config.js --progress".谢谢评论区简友提醒。

优化插件

webpack提供了一些在发布阶段非常有用的优化插件,它们大多来自于webpack社区,可以通过npm安装,通过以下插件可以完成产品发布阶段所需的功能

  • OccurenceOrderPlugin :为组件分配ID,通过这个插件webpack可以分析和优先考虑使用最多的模块,并为它们分配最小的ID
  • UglifyJsPlugin:压缩JS代码;
  • ExtractTextPlugin:分离CSS和JS文件

我们继续用例子来看看如何添加它们,OccurenceOrder 和 UglifyJS plugins 都是内置插件,你需要做的只是安装其它非内置插件

npm install --save-dev extract-text-webpack-plugin

在配置文件的plugins后引用它们

// webpack.production.config.js
const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const ExtractTextPlugin = require('extract-text-webpack-plugin');

module.exports = {
    entry: __dirname + "/app/main.js",//已多次提及的唯一入口文件
    output: {
        path: __dirname + "/build",
        filename: "bundle.js"
    },
    devtool: 'none',
    devServer: {
        contentBase: "./public",//本地服务器所加载的页面所在的目录
        historyApiFallback: true,//不跳转
        inline: true,
        hot: true
    },
    module: {
        rules: [
            {
                test: /(\.jsx|\.js)$/,
                use: {
                    loader: "babel-loader"
                },
                exclude: /node_modules/
            },
            {
                test: /\.css$/,
                use: [
                    {
                        loader: "style-loader"
                    }, {
                        loader: "css-loader",
                        options: {
                            modules: true
                        }
                    }, {
                        loader: "postcss-loader"
                    }
                ]
            }
        ]
    },
    plugins: [
        new webpack.BannerPlugin('版权所有,翻版必究'),
        new HtmlWebpackPlugin({
            template: __dirname + "/app/index.tmpl.html"
        }),
        new webpack.optimize.OccurrenceOrderPlugin(),
        new webpack.optimize.UglifyJsPlugin(),
        new ExtractTextPlugin("style.css")
    ],
};

此时执行npm run build可以看见代码是被压缩后的

压缩后的代码

缓存

缓存无处不在,使用缓存的最好方法是保证你的文件名和文件内容是匹配的(内容改变,名称相应改变)

webpack可以把一个哈希值添加到打包的文件名中,使用方法如下,添加特殊的字符串混合体([name], [id] and [hash])到输出文件名前

const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const ExtractTextPlugin = require('extract-text-webpack-plugin');

module.exports = {
..
    output: {
        path: __dirname + "/build",
        filename: "bundle-[hash].js"
    },
   ...
};

现在用户会有合理的缓存了。

带hash值的js名

去除build文件中的残余文件

添加了hash之后,会导致改变文件内容后重新打包时,文件名不同而内容越来越多,因此这里介绍另外一个很好用的插件clean-webpack-plugin

安装
cnpm install clean-webpack-plugin --save-dev

使用

引入clean-webpack-plugin插件后在配置文件的plugins中做相应配置即可:

const CleanWebpackPlugin = require("clean-webpack-plugin");
  plugins: [
    ...// 这里是之前配置的其它各种插件
    new CleanWebpackPlugin('build/*.*', {
      root: __dirname,
      verbose: true,
      dry: false
  })
  ]

关于clean-webpack-plugin的详细使用可参考这里

总结

其实这是一年前的文章了,趁周末重新运行和修改了一下,现在所有的代码都可以正常运行,所用webpack基于最新的webpack3.5.3。希望依旧能对你有帮助。

这是一篇好长的文章,谢谢你的耐心,能仔细看到了这里,大概半个月前我第一次自己一步步配置项目所需的Webpack后就一直想写一篇笔记做总结,几次动笔都不能让自己满意,总觉得写不清楚。其实关于Webpack本文讲述得仍不完全,不过相信你看完后已经进入Webpack的大门,能够更好的探索其它的关于Webpack的知识了。

欢迎大家在文后发表自己的观点讨论。

更新说明

2017-12-11更新,修改css module部分代码及示例图片,css module真的非常好用,希望大家都能用上。

2017年9月18日更新,添加了一个使用webpack配置多页应用的demo,可以点击此处查看

2017年8月13日更新,本文依据webpack3.5.3将文章涉及代码完全重写,所有代码都在Mac上正常运行过。希望依旧对你学习webpack有帮助。

2017年8月16号更新:
最近在Gitchat上将发起了一场关于webpack的分享,目的在于一起花最短的时间理解和学会webpack,感兴趣的童鞋可以微信扫描注册哈。
webpack从入门到工程实践

查看原文

shinn_lancelot 收藏了文章 · 2019-01-09

『前端好文整理』2019你值得拥有

年初按照惯例,是应该立下flag的时候了。

把2018年积累的一些碎片化的好文都梳理了一遍,知识体系化后学习会变得更加有目的性,从而提升学习效率(github地址)。

JS篇

数据类型

this

作用域链与闭包

原型与原型链

JS执行底层

ES6/ES7..

除此之外强烈推荐冴羽老师的ES6系列文章,深入骨髓的理解ES6中的核心。

TypeScript

Node

HTML/CSS篇

HTTP

性能&优化篇

Webpack篇

React篇

面试篇

一点感悟

在大前端的时代,要学的知识越来越多,唯有不断的学习不断的积累才能走的更远!本文也会不断的更新下去,希望能帮助到更多的前端爱好者。当然如果你有好文章推荐的话,不妨留言评论,大家一起分享,一起成长。Learn and live!奉上github

查看原文

shinn_lancelot 收藏了文章 · 2018-12-28

规范化 Git 版本提交信息和版本发布

规范化 Git 版本提交信息和版本发布

本文讲解了如何利用工具 commitizen/cz-cli + commitizen/cz-conventional-changelog + conventional-changelog/standard-version,规范提交信息和版本发布。

前言

协作开发下不同开发人员的 commit 书写习惯不同,查看 commit 记录时感觉就比较乱且不尽详细。我自己也是,没有一套合适的 commit 规范约束,每次都只是大概写个主要概述就提交,当需要返回查看时就看不出太多信息了。同时 CHANGELOG 也是记录的不尽详细,之前的手动书写不能让我们查看项目的每个发行版之间发生的变化。也所以上网查找了下解决方案,感觉甚好,有必要总结一下。

约定式提交规范

约定式提交规范是基于Angular提交准则形成,提交说明的结构如下:

<类型>[可选的作用域]: <描述>

[可选的正文]

[可选的脚注]

其中,<类型>是为了向类库使用者表明其意图,其可选值为:

  • feat: 表示新增了一个功能
  • fix: 表示修复了一个 bug
  • docs: 表示只修改了文档
  • style: 表示修改格式、书写错误、空格等不影响代码逻辑的操作
  • refactor: 表示修改的代码不是新增功能也不是修改 bug,比如代码重构
  • perf: 表示修改了提升性能的代码
  • test: 表示修改了测试代码
  • build: 表示修改了编译配置文件
  • chore: 无 src 或 test 的操作
  • revert: 回滚操作

[可选的作用域]: 是为了描述 此次 commit 影响的范围,比如: route, component, utils, build, api, website, docs

<描述>: 此次提交的简短描述

[可选的正文]: 此次提交的详细描述,描述为什么修改,做了什么样的修改,以及开发的思路等等,输入 \n 换行

[可选的页脚]: 主要写下面2种

  • Breaking changes: 在可选的正文或脚注的起始位置带有 BREAKING CHANGE: 的提交,表示引入了破坏性变更(这和语义化版本中的 MAJOR 相对应)。
  • Closed issues: 罗列此次提交修复的 bug,如 fixes issue #110

两种形式安装

全局安装

npm install -g commitizen cz-conventional-changelog
echo '{ "path": "cz-conventional-changelog" }' > ~/.czrc

全局模式下, 需要 ~/.czrc 配置文件, 为 commitizen 指定 Adapter

执行

git cz

项目级安装

npm install -D commitizen cz-conventional-changelog

package.json中配置:

"script":{"commit":"git-cz"},
"config":{"commitizen":{"path":"node_modules/cz-conventional-changelog"}}

执行

npm run commit

standard-version: 自动生成 CHANGELOG

同样可以有 2 种安装形式,这里只介绍项目级安装,接下来的介绍也都以此为例。

npm i -S standard-version

package.json 配置:

"scirpt":{"release":"standard-version"}

执行

npm run release

若全局安装,可直接使用 standard-version 命令,其作用等同于npm run release,下面不再赘述

standard-version 介绍

选项:

  --release-as, -r     Specify the release type manually (like npm version <major|minor|patch|1.1.0>) [字符串]
  // major: 1.0.0 -> 2.0.0, minor: 1.0.0 -> 1.1.0, patch : 1.0.0 -> 1.0.1
  --prerelease, -p     make a pre-release with optional option value to specify a tag id [字符串]
  --infile, -i         Read the CHANGELOG from this file                 [默认值: "CHANGELOG.md"]
  --message, -m        Commit message, replaces %s with new version [字符串] [默认值: "chore(release): %s"]
  --first-release, -f  Is this the first release?                          [布尔] [默认值: false]
  --sign, -s           Should the git commit and tag be signed?            [布尔] [默认值: false]
  --no-verify, -n      Bypass pre-commit or commit-msg git hooks during the commit phase [布尔] [默认值: false]
  --commit-all, -a     Commit all staged changes, not just files affected by standard-version [布尔] [默认值: false]
  --silent             Don't print logs and errors                         [布尔] [默认值: false]
  --tag-prefix, -t     Set a custom prefix for the git tag to be created   [字符串] [默认值: "v"]
  --scripts            Provide scripts to execute for lifecycle events (prebump, precommit, [默认值: {}]
  --skip               Map of steps in the release process that should be skipped    [默认值: {}]
  --dry-run            See the commands that running standard-version would run [布尔] [默认值: false]

常用发布命令

// 初次发布版本
npm run release --first-release
// 添加版本信息和指定发布版本等级
npm run release -m "Commit message" -r minor

// 确认发布,npm publish 发布到 npm
git push --follow-tags origin master && npm publish

参考链接

优雅的提交你的 Git Commit Message

Standard Version

commitizen/cz-cli

查看原文

shinn_lancelot 赞了问题 · 2018-12-21

解决为什么要使用CSS预处理器?

最近了解了一些css的预处理器,如:LESS,SASS等等。但是发现在自己的项目中很少使用到。想了解一下多人合作中需要用到CSS预处理器吗?有什么好处?推荐使用那一款?

关注 17 回答 5

shinn_lancelot 收藏了问题 · 2018-12-21

为什么要使用CSS预处理器?

最近了解了一些css的预处理器,如:LESS,SASS等等。但是发现在自己的项目中很少使用到。想了解一下多人合作中需要用到CSS预处理器吗?有什么好处?推荐使用那一款?

shinn_lancelot 收藏了文章 · 2018-12-05

解决 font-weight 无效的问题

图片描述

近期调页面时有几个 font-weight 需要修改,无论怎么调整字体粗细都没有变化,深入研究后总结下文

初探

本地写个例子,代码如下

<p class="thin">This is a paragraph</p>
<p class="normal">This is a paragraph</p>
<p class="thick">This is a paragraph</p>
p {
  font-size: 50px;
}
p.thin {
  font-weight: 100;
}
p.normal {
  font-weight: normal;
}
p.thick {
  font-weight: bold;
}

在 Mac OS 下 Chrome、Firefox、Safari 结果分别如下(从左到右)

图片描述

我的浏览器均为最新版本,发现一个简单的 font-weight 属性,在三个浏览器有三个表现。

  • Chrome 下所有字重均一样
  • Firefox 下表现正常,是我们期待的结果
  • Safari 下 100 无效,被解析为 normal

解决表现不一致的问题

这种不同浏览器表现不同是我们不能接受的,对于后期排错造成困难,于是我首先想到是字体的惹得货,修改我的样式文件

p {
  font-size: 50px;
  font-family: Arial;
}
p.thin {
  font-weight: 100;
}
p.normal {
  font-weight: normal;
}
p.thick {
  font-weight: bold;
}

效果如下

图片描述

这里的表现倒是一样的,我们可以忽略图中字体大小(截屏的误差导致),只看字体粗细就好,font-weight: 100; 都失效了。

MDN 文档的解释

This means that for fonts that provide only normal and bold, 100-500 are normal, and 600-900 are bold.

文章开始没有介绍基本语法,相信前端们都知道,normal 等同于 400, bold 等同于 700。

这也很好的解释了这个例子的表象,但我瞬间推翻了这句话,因为在例子1中 Firefox 在没有设置字体的情况下可以正常显示。

问题根源

到这里相信你已经知道答案了,我们要针对不同浏览器和运行环境进行全面配置 font-family 属性,全局的字体建议放在 body 选择器下

p {
  font-size: 50px;
  font-family: -apple-system, BlinkMacSystemFont,
    "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell",
    "Fira Sans", "Droid Sans", "Helvetica Neue",
    sans-serif;
}
p.thin {
  font-weight: 100;
}
p.normal {
  font-weight: normal;
}
p.thick {
  font-weight: bold;
}

看下三个浏览器的表现

图片描述

在字体和字重上达到了完全一致,仔细的观察会发现,Chrome 与 Safari 渲染不同字重的字体总宽度变化明显,而 Firefox 下则不是十分明显

温馨提示:尽量不要用字体去撑容器的宽度,尽量避免 hover 改变字重。因为不同环境下渲染的差异会导致表现不一致。

上面给的一大串字体又代表兼容那些环境和设备哪?

首先我们分成三组来解释

font-family:
/* 1 */ -apple-system, BlinkMacSystemFont,
/* 2 */ "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans",
/* 3 */ "Helvetica Neue", sans-serif;

1.第一个分组是映射到系统 UI 字体的 CSS 属性。这涵盖了很多环境,并且不会将这些字体误认为别的字体

  • -apple-system 在 Mac OS X 和 iOS 上的 Safari 中设置 San Francisco,并在旧版本的 Mac OS X 上设置成 Neue Helvetica 和 Lucida Grande。它根据字体大小正确选择 San Francisco Text 和 San Francisco Display。
  • BlinkMacSystemFont 只针对于 Mac OS X 上的 Chrome。

2.第二个分组用于已知的系统 UI 字体

  • Segoe UI 针对 Windows 和 Windows Phone。
  • Roboto 针对 Android 和更高版本的 Chrome 操作系统。故意列出在 Segoe UI 后,因为如果你是 Windows 上的 Android 开发人员,并安装 Roboto,将使用 Segoe UI。
  • Oxygen 针对 KDE,Ubuntu,你可以猜到,Cantarell 针对 GNOME。这一开始感到徒劳,因为一些 Linux 发行版有许多这样的字体。
  • Fira Sans 针对 Firefox OS 系统。
  • Droid Sans 针对旧版本系统的安卓
请注意,我们不需要添加 San Francisco。在 iOS 和 Mac OS X 上,San Francisco 并不是显而易见的,而是作为“隐藏”字体存在。我们也不使用 .SFNSText-Regular,在 Mac OS X 上的 San Francisco 的内部 PostScript 名称来指定 San Francisco。它只适用于 Chrome,并且不如 BlinkMacSystemFont 通用。

3.第三个分组是我们的后备字体

  • Helvetica Neue 针对旧 El Capitan 版本的 Mac OS X。它被列在接近结尾,因为它是一个流行的字体在其他非 El Capitan 计算机上。
  • sans-serif 默认的是 sans-serif 后备字体。

以下是目前已知的的问题:

  1. 在 Mac OS X 的 Firefox 中,San Francisco 的字母间距比 Safari 和 Chrome 更紧。
  2. 它不会使 Lucida Grande 在 Mac OS X 的 pre-Yosemite 版本上降级到 Neue Helvetica。并且它可能不匹配不太受欢迎的操作系统上的正确字体或更复杂的配置。

说到这里上面都是英文的字体,我们需要针对中文设置字体,可以针对不同操作系统给中文字体。

总结

对于字体的统一展示,目前为止还没有完善的解决办法,只能针对不同设备给出对应的解决方案,至于为什么不引入外部的三方字库来统一字体呢?因为会增加网页的请求时长,渲染也会耗时,尽量避免三方字库。下次再有类似字重渲染误差问题可以先从字体下手,整个例子没有跑过 Windows 系统,可能在 Windows 下还会有问题。至少切入点有了改变,并不是 Chrome 下 font-weight 无效。

文章出自 orange 的 个人博客 http://orangexc.xyz/
查看原文

shinn_lancelot 收藏了文章 · 2018-11-30

2018 年了,你还是只会 npm install 吗

nodejs 社区乃至 Web 前端工程化领域发展到今天,作为 node 自带的包管理工具的 npm 已经成为每个前端开发者必备的工具。但是现实状况是,我们很多人对这个nodejs基础设施的使用和了解还停留在: 会用 npm install 这里(一言不合就删除整个 node_modules 目录然后重新 install 这种事你没做过吗?)

当然 npm 能成为现在世界上最大规模的包管理系统,很大程度上确实归功于它足够用户友好,你看即使我只会执行 install 也不必太担心出什么大岔子. 但是 npm 的功能远不止于 install 一下那么简单,这篇文章帮你扒一扒那些你可能不知道的 npm 原理、特性、技巧,以及(我认为的)最佳实践。

你懒得读的 npm 文档,我帮你翻译然后试验整理过来了 ???

1. npm init

我们都知道 package.json 文件是用来定义一个 package 的描述文件, 也知道npm init 命令用来初始化一个简单的 package.json 文件,执行该命令后终端会依次询问 name, version, description 等字段。

1.1 npm init 执行默认行为


而如果想要偷懒步免去一直按 enter,在命令后追加 --yes 参数即可,其作用与一路下一步相同。

npm init --yes

1.2 自定义 npm init 行为

npm init 命令的原理并不复杂,调用脚本,输出一个初始化的 package.json 文件就是了。所以相应地,定制 npm init 命令的实现方式也很简单,在 Home 目录创建一个 .npm-init.js 即可,该文件的 module.exports 即为 package.json 配置内容,需要获取用户输入时候,使用 prompt() 方法即可。

例如编写这样的 ~/.npm-init.js

const desc = prompt('description?', 'A new package...')
const bar = prompt('bar?', '')
const count = prompt('count?', '42')

module.exports = {
  key: 'value',
  foo: {
    bar: bar,
    count: count
  },
  name: prompt('name?', process.cwd().split('/').pop()),
  version: prompt('version?', '0.1.0'),
  description: desc,
  main: 'index.js',
}

此时在 ~/hello 目录下执行 npm init 将会得到这样的 package.json:

{
  "key": "value",
  "foo": {
    "bar": "",
    "count": "42"
  },
  "name": "hello",
  "version": "0.1.0",
  "description": "A new package...",
  "main": "index.js"
}

除了生成 package.json, 因为 .npm-init.js 是一个常规的模块,意味着我们可以执行随便什么 node 脚本可以执行的任务。例如通过 fs 创建 README, .eslintrc 等项目必需文件,实现项目脚手架的作用。

2. 依赖包安装

依赖管理是 npm 的核心功能,原理就是执行 npm install 从 package.json 中的 dependencies, devDependencies 将依赖包安装到当前目录的 ./node_modules 文件夹中。

2.1 package定义

我们都知道要手动安装一个包时,执行 npm install <package> 命令即可。这里的第三个参数 package 通常就是我们所要安装的包名,默认配置下 npm 会从默认的源 (Registry) 中查找该包名对应的包地址,并下载安装。但在 npm 的世界里,除了简单的指定包名, package 还可以是一个指向有效包名的 http url/git url/文件夹路径。

阅读 npm的文档, 我们会发现package 准确的定义,只要符合以下 a) 到 g) 其中之一条件,就是一个 package:

#说明例子
a)一个包含了程序和描述该程序的 package.json 文件 的 文件夹./local-module/
b)一个包含了 (a) 的 gzip 压缩文件./module.tar.gz
c)一个可以下载得到 (b) 资源的 url (通常是 http(s) url)https://registry.npmjs.org/we...
d)一个格式为 <name>@<version> 的字符串,可指向 npm 源(通常是官方源 npmjs.org)上已发布的可访问 url,且该 url 满足条件 (c)webpack@4.1.0
e)一个格式为 <name>@<tag> 的字符串,在 npm 源上该<tag>指向某 <version> 得到 <name>@<version>,后者满足条件 (d)webpack@latest
f)一个格式为 <name> 的字符串,默认添加 latest 标签所得到的 <name>@latest 满足条件 (e)webpack
g)一个 git url, 该 url 所指向的代码库满足条件 (a)git@github.com:webpack/webpack.git

2.2 安装本地包/远程git仓库包

上面表格的定义意味着,我们在共享依赖包时,并不是非要将包发表到 npm 源上才可以提供给使用者来安装。这对于私有的不方便 publish 到远程源(即使是私有源),或者需要对某官方源进行改造,但依然需要把包共享出去的场景来说非常实用。

场景1: 本地模块引用

nodejs 应用开发中不可避免有模块间调用,例如在实践中经常会把需要被频繁引用的配置模块放到应用根目录;于是在创建了很多层级的目录、文件后,很可能会遇到这样的代码:

const config = require('../../../../config.js');

除了看上去很丑以外,这样的路径引用也不利于代码的重构。并且身为程序员的自我修养告诉我们,这样重复的代码多了也就意味着是时候把这个模块分离出来供应用内其他模块共享了。例如这个例子里的 config.js 非常适合封装为 package 放到 node_modules 目录下,共享给同应用内其他模块。

无需手动拷贝文件或者创建软链接到 node_modules 目录,npm 有更优雅的解决方案。

方案:

  1. 创建 config 包:
    新增 config 文件夹; 重命名 config.js 为 config/index.js 文件; 创建 package.json 定义 config 包

    {
        "name": "config",
        "main": "index.js",
        "version": "0.1.0"
    }
  2. 在应用层 package.json 文件中新增依赖项,然后执行 npm install; 或直接执行第 3 步

    {
        "dependencies": {
            "config": "file:./config"
        }
    }
  3. (等价于第 2 步)直接在应用目录执行 npm install file:./config

    此时,查看 node_modules 目录我们会发现多出来一个名为 config,指向上层 config/ 文件夹的软链接。这是因为 npm 识别 file: 协议的url,得知这个包需要直接从文件系统中获取,会自动创建软链接到 node_modules 中,完成“安装”过程。

    相比手动软链,我们既不需要关心 windows 和 linux 命令差异,又可以显式地将依赖信息固化到 dependencies 字段中,开发团队其他成员可以执行 npm install 后直接使用。

场景2: 私有 git 共享 package

有些时候,我们一个团队内会有一些代码/公用库需要在团队内不同项目间共享,但可能由于包含了敏感内容,或者代码太烂拿不出手等原因,不方便发布到源。

这种情况下,我们可以简单地将被依赖的包托管在私有的 git 仓库中,然后将该 git url 保存到 dependencies 中. npm 会直接调用系统的 git 命令从 git 仓库拉取包的内容到 node_modules 中。

npm 支持的 git url 格式:

<protocol>://[<user>[:<password>]@]<hostname>[:<port>][:][/]<path>[#<commit-ish> | #semver:<semver>]

git 路径后可以使用 # 指定特定的 git branch/commit/tag, 也可以 #semver: 指定特定的 semver range.

例如:

git+ssh://git@github.com:npm/npm.git#v1.0.27
git+ssh://git@github.com:npm/npm#semver:^5.0
git+https://isaacs@github.com/npm/npm.git
git://github.com/npm/npm.git#v1.0.27

场景3: 开源 package 问题修复

使用某个 npm 包时发现它有某个严重bug,但也许最初作者已不再维护代码了,也许我们工作紧急,没有足够的时间提 issue 给作者再慢慢等作者发布新的修复版本到 npm 源。

此时我们可以手动进入 node_modules 目录下修改相应的包内容,也许修改了一行代码就修复了问题。但是这种做法非常不明智!

首先 node_modules 本身不应该放进版本控制系统,对 node_modules 文件夹中内容的修改不会被记录进 git 提交记录;其次,就算我们非要反模式,把 node_modules 放进版本控制中,你的修改内容也很容易在下次 team 中某位成员执行 npm installnpm update 时被覆盖,而这样的一次提交很可能包含了几十几百个包的更新,你自己所做的修改很容易就被淹没在庞大的 diff 文件列表中了。

方案:

最好的办法应当是 fork 原作者的 git 库,在自己所属的 repo 下修复问题后,将 dependencies 中相应的依赖项更改为自己修复后版本的 git url 即可解决问题。(Fork 代码库后,也便于向原作者提交 PR 修复问题。上游代码库修复问题后,再次更新我们的依赖配置也不迟。)

3. npm install 如何工作 —— node_modules 目录结构

npm install 执行完毕后,我们可以在 node_modules 中看到所有依赖的包。虽然使用者无需关注这个目录里的文件夹结构细节,只管在业务代码中引用依赖包即可,但了解 node_modules 的内容可以帮我们更好理解 npm 如何工作,了解从 npm 2 到 npm 5 有哪些变化和改进。

为简单起见,我们假设应用目录为 app, 用两个流行的包 webpack, nconf 作为依赖包做示例说明。并且为了正常安装,使用了“上古” npm 2 时期的版本 webpack@1.15.0, nconf@0.8.5.

3.1 npm 2

npm 2 在安装依赖包时,采用简单的递归安装方法。执行 npm install 后,npm 2 依次递归安装 webpacknconf 两个包到 node_modules 中。执行完毕后,我们会看到 ./node_modules 这层目录只含有这两个子目录。

node_modules/
├── nconf/
└── webpack/

进入更深一层 nconf 或 webpack 目录,将看到这两个包各自的 node_modules 中,已经由 npm 递归地安装好自身的依赖包。包括 ./node_modules/webpack/node_modules/webpack-core , ./node_modules/conf/node_modules/async 等等。而每一个包都有自己的依赖包,每个包自己的依赖都安装在了自己的 node_modules 中。依赖关系层层递进,构成了一整个依赖树,这个依赖树与文件系统中的文件结构树刚好层层对应。

最方便的查看依赖树的方式是直接在 app 目录下执行 npm ls 命令。

app@0.1.0
├─┬ nconf@0.8.5
│ ├── async@1.5.2
│ ├── ini@1.3.5
│ ├── secure-keys@1.0.0
│ └── yargs@3.32.0
└─┬ webpack@1.15.0
  ├── acorn@3.3.0
  ├── async@1.5.2
  ├── clone@1.0.3
  ├── ...
  ├── optimist@0.6.1
  ├── supports-color@3.2.3
  ├── tapable@0.1.10
  ├── uglify-js@2.7.5
  ├── watchpack@0.2.9
  └─┬ webpack-core@0.6.9
    ├── source-list-map@0.1.8
    └── source-map@0.4.4

这样的目录结构优点在于层级结构明显,便于进行傻瓜式的管理:

  1. 例如新装一个依赖包,可以立即在第一层 node_modules 中看到子目录
  2. 在已知所需包名和版本号时,甚至可以从别的文件夹手动拷贝需要的包到 node_modules 文件夹中,再手动修改 package.json 中的依赖配置
  3. 要删除这个包,也可以简单地手动删除这个包的子目录,并删除 package.json 文件中相应的一行即可

实际上,很多人在 npm 2 时代也的确都这么实践过,的确也都可以安装和删除成功,并不会导致什么差错。

但这样的文件结构也有很明显的问题:

  1. 对复杂的工程, node_modules 内目录结构可能会太深,导致深层的文件路径过长而触发 windows 文件系统中,文件路径不能超过 260 个字符长的错误
  2. 部分被多个包所依赖的包,很可能在应用 node_modules 目录中的很多地方被重复安装。随着工程规模越来越大,依赖树越来越复杂,这样的包情况会越来越多,造成大量的冗余。

——在我们的示例中就有这个问题,webpacknconf 都依赖 async 这个包,所以在文件系统中,webpack 和 nconf 的 node_modules 子目录中都安装了相同的 async 包,并且是相同的版本。

+-------------------------------------------+
|                   app/                    |
+----------+------------------------+-------+
           |                        |
           |                        |
+----------v------+       +---------v-------+
|                 |       |                 |
|  webpack@1.15.0 |       |  nconf@0.8.5    |
|                 |       |                 |
+--------+--------+       +--------+--------+
         |                         |
   +-----v-----+             +-----v-----+
   |async@1.5.2|             |async@1.5.2|
   +-----------+             +-----------+

3.2 npm 3 - 扁平结构

主要为了解决以上问题,npm 3 的 node_modules 目录改成了更加扁平状的层级结构。文件系统中 webpack, nconf, async 的层级关系变成了平级关系,处于同一级目录中。

         +-------------------------------------------+
         |                   app/                    |
         +-+---------------------------------------+-+
           |                                       |
           |                                       |
+----------v------+    +-------------+   +---------v-------+
|                 |    |             |   |                 |
|  webpack@1.15.0 |    | async@1.5.2 |   |  nconf@0.8.5    |
|                 |    |             |   |                 |
+-----------------+    +-------------+   +-----------------+

虽然这样一来 webpack/node_modules 和 nconf/node_modules 中都不再有 async 文件夹,但得益于 node 的模块加载机制,他们都可以在上一级 node_modules 目录中找到 async 库。所以 webpack 和 nconf 的库代码中 require('async') 语句的执行都不会有任何问题。

这只是最简单的例子,实际的工程项目中,依赖树不可避免地会有很多层级,很多依赖包,其中会有很多同名但版本不同的包存在于不同的依赖层级,对这些复杂的情况, npm 3 都会在安装时遍历整个依赖树,计算出最合理的文件夹安装方式,使得所有被重复依赖的包都可以去重安装。

npm 文档提供了更直观的例子解释这种情况:

假如 package{dep} 写法代表包和包的依赖,那么 A{B,C}, B{C}, C{D} 的依赖结构在安装之后的 node_modules 是这样的结构:
A
+-- B
+-- C
+-- D

这里之所以 D 也安装到了与 B C 同一级目录,是因为 npm 会默认会在无冲突的前提下,尽可能将包安装到较高的层级。

如果是 A{B,C}, B{C,D@1}, C{D@2} 的依赖关系,得到的安装后结构是:
A
+-- B
+-- C
   `-- D@2
+-- D@1

这里是因为,对于 npm 来说同名但不同版本的包是两个独立的包,而同层不能有两个同名子目录,所以其中的 D@2 放到了 C 的子目录而另一个 D@1 被放到了再上一层目录。

很明显在 npm 3 之后 npm 的依赖树结构不再与文件夹层级一一对应了。想要查看 app 的直接依赖项,要通过 npm ls 命令指定 --depth 参数来查看:

npm ls --depth 1
PS: 与本地依赖包不同,如果我们通过 npm install --global 全局安装包到全局目录时,得到的目录依然是“传统的”目录结构。而如果使用 npm 3 想要得到“传统”形式的本地 node_modules 目录,使用 npm install --global-style 命令即可。

3.3 npm 5 - package-lock 文件

npm 5 发布于 2017 年也是目前最新的 npm 版本,这一版本依然沿用 npm 3 之后扁平化的依赖包安装方式,此外最大的变化是增加了 package-lock.json 文件。

package-lock.json 的作用是锁定依赖安装结构,如果查看这个 json 的结构,会发现与 node_modules 目录的文件层级结构是一一对应的。

以依赖关系为: app{webpack} 的 'app' 项目为例, 其 package-lock 文件包含了这样的片段。

{
    "name":  "app",
    "version":  "0.1.0",
    "lockfileVersion":  1,
    "requires":  true,
    "dependencies": {
        // ... 其他依赖包
        "webpack": {
            "version": "1.8.11",
            "resolved": "https://registry.npmjs.org/webpack/-/webpack-1.8.11.tgz",
            "integrity": "sha1-Yu0hnstBy/qcKuanu6laSYtgkcI=",
            "requires": {
                "async": "0.9.2",
                "clone": "0.1.19",
                "enhanced-resolve": "0.8.6",
                "esprima": "1.2.5",
                "interpret": "0.5.2",
                "memory-fs": "0.2.0",
                "mkdirp": "0.5.1",
                "node-libs-browser": "0.4.3",
                "optimist": "0.6.1",
                "supports-color": "1.3.1",
                "tapable": "0.1.10",
                "uglify-js": "2.4.24",
                "watchpack": "0.2.9",
                "webpack-core": "0.6.9"
            }
        },
        "webpack-core": {
            "version": "0.6.9",
            "resolved": "https://registry.npmjs.org/webpack-core/-/webpack-core-0.6.9.tgz",
            "integrity": "sha1-/FcViMhVjad76e+23r3Fo7FyvcI=",
            "requires": {
                "source-list-map": "0.1.8",
                "source-map": "0.4.4"
            },
            "dependencies": {
                "source-map": {
                    "version": "0.4.4",
                    "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.4.4.tgz",
                    "integrity": "sha1-66T12pwNyZneaAMti092FzZSA2s=",
                    "requires": {
                        "amdefine": "1.0.1"
                    }
                }
            }
        },
        //... 其他依赖包
    }
}

看懂 package-lock 文件并不难,其结构是同样类型的几个字段嵌套起来的,主要是 version, resolved, integrity, requires, dependencies 这几个字段而已。

  • version, resolved, integrity 用来记录包的准确版本号、内容hash、安装源的,决定了要安装的包的准确“身份”信息
  • 假设盖住其他字段,只关注文件中的 dependencies: {} 我们会发现,整个文件的 JSON 配置里的 dependencies 层次结构与文件系统中 node_modules 的文件夹层次结构是完全对照的
  • 只关注 requires: {} 字段又会发现,除最外层的 requires 属性为 true 以外, 其他层的 requires 属性都对应着这个包的 package.json 里记录的自己的依赖项

因为这个文件记录了 node_modules 里所有包的结构、层级和版本号甚至安装源,它也就事实上提供了 “保存” node_modules 状态的能力。只要有这样一个 lock 文件,不管在那一台机器上执行 npm install 都会得到完全相同的 node_modules 结果。

这就是 package-lock 文件致力于优化的场景:在从前仅仅用 package.json 记录依赖,由于 semver range 的机制;一个月前由 A 生成的 package.json 文件,B 在一个月后根据它执行 npm install 所得到的 node_modules 结果很可能许多包都存在不同的差异,虽然 semver 机制的限制使得同一份 package.json 不会得到大版本不同的依赖包,但同一份代码在不同环境安装出不同的依赖包,依然是可能导致意外的潜在因素。

相同作用的文件在 npm 5 之前就有,称为 npm shrinkwrap 文件,二者作用完全相同,不同的是后者需要手动生成,而 npm 5 默认会在执行 npm install 后就生成 package-lock 文件,并且建议你提交到 git/svn 代码库中。

package-lock.json 文件在最初 npm 5.0 默认引入时也引起了相当大的争议。在 npm 5.0 中,如果已有 package-lock 文件存在,若手动在 package.json 文件新增一条依赖,再执行 npm install, 新增的依赖并不会被安装到 node_modules 中, package-lock.json 也不会做相应的更新。这样的表现与使用者的自然期望表现不符。在 npm 5.1 的首个 Release 版本中这个问题得以修复。这个事情告诉我们,要升级,不要使用 5.0。

——但依然有反对的声音认为 package-lock 太复杂,对此 npm 也提供了禁用配置:

npm config set package-lock false

4. 依赖包版本管理

依赖包安装完并不意味着就万事大吉了,版本的维护和更新也很重要。这一章介绍依赖包升级管理相关知识,太长不看版本请直接跳到 4.3 最佳实践

4.1 semver

npm 依赖管理的一个重要特性是采用了语义化版本 (semver) 规范,作为依赖版本管理方案。

semver 约定一个包的版本号必须包含3个数字,格式必须为 MAJOR.MINOR.PATCH, 意为 主版本号.小版本号.修订版本号.

  • MAJOR 对应大的版本号迭代,做了不兼容旧版的修改时要更新 MAJOR 版本号
  • MINOR 对应小版本迭代,发生兼容旧版API的修改或功能更新时,更新MINOR版本号
  • PATCH 对应修订版本号,一般针对修复 BUG 的版本号

对于包作者(发布者),npm 要求在 publish 之前,必须更新版本号。npm 提供了 npm version 工具,执行 npm version major|minor|patch 可以简单地将版本号中相应的数字加1.

如果包是一个 git 仓库,npm version 还会自动创建一条注释为更新后版本号的 git commit 和名为该版本号的 tag

对于包的引用者来说,我们需要在 dependencies 中使用 semver 约定的 semver range 指定所需依赖包的版本号或版本范围。npm 提供了网站 https://semver.npmjs.com 可方便地计算所输入的表达式的匹配范围。常用的规则示例如下表:

range含义
^2.2.1指定的 MAJOR 版本号下, 所有更新的版本匹配 2.2.3, 2.3.0; 不匹配 1.0.3, 3.0.1
~2.2.1指定 MAJOR.MINOR 版本号下,所有更新的版本匹配 2.2.3, 2.2.9 ; 不匹配 2.3.0, 2.4.5
>=2.1版本号大于或等于 2.1.0匹配 2.1.2, 3.1
<=2.2版本号小于或等于 2.2匹配 1.0.0, 2.2.1, 2.2.11
1.0.0 - 2.0.0版本号从 1.0.0 (含) 到 2.0.0 (含)匹配 1.0.0, 1.3.4, 2.0.0

任意两条规则,通过 || 连接起来,则表示两条规则的并集:

^2 >=2.3.1 || ^3 >3.2 可以匹配:

* `2.3.1`, `2,8.1`, `3.3.1`
* 但不匹配 `1.0.0`, `2.2.0`, `3.1.0`, `4.0.0`

PS: 除了这几种,还有如下更直观的表示版本号范围的写法:

  • *x 匹配所有主版本
  • 11.x 匹配 主版本号为 1 的所有版本
  • 1.21.2.x 匹配 版本号为 1.2 开头的所有版本

PPS: 在常规仅包含数字的版本号之外,semver 还允许在 MAJOR.MINOR.PATCH 后追加 - 后跟点号分隔的标签,作为预发布版本标签 - Prerelese Tags,通常被视为不稳定、不建议生产使用的版本。例如:

  • 1.0.0-alpha
  • 1.0.0-beta.1
  • 1.0.0-rc.3

上表中我们最常见的是 ^1.8.11 这种格式的 range, 因为我们在使用 npm install <package name> 安装包时,npm 默认安装当前最新版本,例如 1.8.11, 然后在所安装的版本号前加^号, 将 ^1.8.11 写入 package.json 依赖配置,意味着可以匹配 1.8.11 以上,2.0.0 以下的所有版本。

4.2 依赖版本升级

问题来了,在安装完一个依赖包之后有新版本发布了,如何使用 npm 进行版本升级呢?——答案是简单的 npm installnpm update,但在不同的 npm 版本,不同的 package.json, package-lock.json 文件,安装/升级的表现也不同。

我们不妨还以 webpack 举例,做如下的前提假设:

  • 我们的工程项目 app 依赖 webpack
  • 项目最初初始化时,安装了当时最新的包 webpack@1.8.0,并且 package.json 中的依赖配置为: "webpack": "^1.8.0"
  • 当前(2018年3月) webpack 最新版本为 4.2.0, webpack 1.x 最新子版本为 1.15.0

如果我们使用的是 npm 3, 并且项目不含 package-lock.json, 那么根据 node_modules 是否为空,执行 install/update 的结果如下 (node 6.13.1, npm 3.10.10 环境下试验):

#package.json (BEFORE)node_modules (BEFORE)command (npm 3)package.json (AFTER)node_modules (AFTER)
a)webpack: ^1.8.0webpack@1.8.0installwebpack: ^1.8.0webpack@1.8.0
b)webpack: ^1.8.0installwebpack: ^1.8.0webpack@1.15.0
c)webpack: ^1.8.0webpack@1.8.0updatewebpack: ^1.8.0webpack@1.15.0
d)webpack: ^1.8.0updatewebpack: ^1.8.0webpack@1.15.0

根据这个表我们可以对 npm 3 得出以下结论:

  • 如果本地 node_modules 已安装,再次执行 install 不会更新包版本, 执行 update 才会更新; 而如果本地 node_modules 为空时,执行 install/update 都会直接安装更新包;
  • npm update 总是会把包更新到符合 package.json 中指定的 semver 的最新版本号——本例中符合 ^1.8.0 的最新版本为 1.15.0
  • 一旦给定 package.json, 无论后面执行 npm install 还是 update, package.json 中的 webpack 版本一直顽固地保持 一开始的 ^1.8.0 岿然不动

这里不合理的地方在于,如果最开始团队中第一个人安装了 webpack@1.8.0, 而新加入项目的成员, checkout 工程代码后执行 npm install 会安装得到不太一样的 1.15.0 版本。虽然 semver 约定了小版本号应当保持向下兼容(相同大版本号下的小版本号)兼容,但万一有不熟悉不遵循此约定的包发布者,发布了不兼容的包,此时就可能出现因依赖环境不同导致的 bug。

下面由 npm 5 带着 package-lock.json 闪亮登场,执行 install/update 的效果是这样的 (node 9.8.0, npm 5.7.1 环境下试验):

下表为表述简单,省略了包名 webpack, install 简写 i, update 简写为 up
#package.json (BEFORE)node_modules (BEFORE)package-lock (BEFORE)commandpackage.json (AFTER)node_modules (AFTER)
a)^1.8.0@1.8.0@1.8.0i^1.8.0@1.8.0
b)^1.8.0@1.8.0i^1.8.0@1.8.0
c)^1.8.0@1.8.0@1.8.0up^1.15.0@1.15.0
d)^1.8.0@1.8.0up^1.8.0@1.15.0
e)^1.15.0@1.8.0 (旧)@1.15.0i^1.15.0@1.15.0
f)^1.15.0@1.8.0 (旧)@1.15.0up^1.15.0@1.15.0

与 npm 3 相比,在安装和更新依赖版本上主要的区别为:

  • 无论何时执行 install, npm 都会优先按照 package-lock 中指定的版本来安装 webpack; 避免了 npm 3 表中情形 b) 的状况;
  • 无论何时完成安装/更新, package-lock 文件总会跟着 node_modules 更新 —— (因此可以视 package-lock 文件为 node_modules 的 JSON 表述)
  • 已安装 node_modules 后若执行 npm update,package.json 中的版本号也会随之更改为 ^1.15.0

由此可见 npm 5.1 使得 package.json 和 package-lock.json 中所保存的版本号更加统一,解决了 npm 之前的各种问题。只要遵循好的实践习惯,团队成员可以很方便地维护一套应用代码和 node_modules 依赖都一致的环境。

皆大欢喜。

4.3 最佳实践

总结起来,在 2018 年 (node 9.8.0, npm 5.7.1) 时代,我认为的依赖版本管理应当是:

  • 使用 npm: >=5.1 版本, 保持 package-lock.json 文件默认开启配置
  • 初始化:第一作者初始化项目时使用 npm install <package> 安装依赖包, 默认保存 ^X.Y.Z 依赖 range 到 package.json中; 提交 package.json, package-lock.json, 不要提交node_modules 目录
  • 初始化:项目成员首次 checkout/clone 项目代码后,执行一次npm install 安装依赖包
  • 不要手动修改 package-lock.json
  • 升级依赖包:

    • 升级小版本: 本地执行 npm update 升级到新的小版本
    • 升级大版本: 本地执行 npm install <package-name>@<version> 升级到新的大版本
    • 也可手动修改 package.json 中版本号为要升级的版本(大于现有版本号)并指定所需的 semver, 然后执行 npm install
    • 本地验证升级后新版本无问题后,提交新的 package.json, package-lock.json 文件
  • 降级依赖包:

    • 正确: npm install <package-name>@<old-version> 验证无问题后,提交 package.json 和 package-lock.json 文件
    • 错误: 手动修改 package.json 中的版本号为更低版本的 semver, 这样修改并不会生效,因为再次执行 npm install 依然会安装 package-lock.json 中的锁定版本
  • 删除依赖包:

    • Plan A: npm uninstall <package> 并提交 package.jsonpackage-lock.json
    • Plan B: 把要卸载的包从 package.json 中 dependencies 字段删除, 然后执行 npm install 并提交 package.jsonpackage-lock.json
  • 任何时候有人提交了 package.json, package-lock.json 更新后,团队其他成员应在 svn update/git pull 拉取更新后执行 npm install 脚本安装更新后的依赖包

恭喜你终于可以跟 rm -rf node_modules && npm install 这波操作说拜拜了(其实并不会)

5. npm scripts

5.1 基本使用

npm scripts 是 npm 另一个很重要的特性。通过在 package.json 中 scripts 字段定义一个脚本,例如:

{
    "scripts": {
        "echo": "echo HELLO WORLD"
    }
}

我们就可以通过 npm run echo 命令来执行这段脚本,像在 shell 中执行该命令 echo HELLO WORLD 一样,看到终端输出 HELLO WORLD.

—— npm scripts 的基本使用就是这么简单,它提供了一个简单的接口用来调用工程相关的脚本。关于更详细的相关信息,可以参考阮一峰老师的文章 npm script 使用指南 (2016年10月).

简要总结阮老师文章内容:

  1. npm run 命令执行时,会把 ./node_modules/.bin/ 目录添加到执行环境的 PATH 变量中,因此如果某个命令行包未全局安装,而只安装在了当前项目的 node_modules 中,通过 npm run 一样可以调用该命令。
  2. 执行 npm 脚本时要传入参数,需要在命令后加 -- 标明, 如 npm run test -- --grep="pattern" 可以将 --grep="pattern" 参数传给 test 命令
  3. npm 提供了 pre 和 post 两种钩子机制,可以定义某个脚本前后的执行脚本
  4. 运行时变量:在 npm run 的脚本执行环境内,可以通过环境变量的方式获取许多运行时相关信息,以下都可以通过 process.env 对象访问获得:

    • npm_lifecycle_event - 正在运行的脚本名称
    • npm_package_<key> - 获取当前包 package.json 中某个字段的配置值:如 npm_package_name 获取包名
    • npm_package_<key>_<sub-key> - package.json 中嵌套字段属性:如 npm_pacakge_dependencies_webpack 可以获取到 package.json 中的 dependencies.webpack 字段的值,即 webpack 的版本号

5.2 node_modules/.bin 目录

上面所说的 node_modules/.bin 目录,保存了依赖目录中所安装的可供调用的命令行包。

何谓命令行包?例如 webpack 就属于一个命令行包。如果我们在安装 webpack 时添加 --global 参数,就可以在终端直接输入 webpack 进行调用。但如果不加 --global 参数,我们会在 node_modules/.bin 目录里看到名为 webpack 的文件,如果在终端直接输入 ./node_modules/.bin/webpack 命令,一样可以执行。

这是因为 webpackpackage.json 文件中定义了 bin 字段为:

{
    "bin": {
        "webpack": "./bin/webpack.js"
    }
}

bin 字段的配置格式为: <command>: <file>, 即 命令名: 可执行文件. npm 执行 install 时,会分析每个依赖包的 package.json 中的 bin 字段,并将其包含的条目安装到 ./node_modules/.bin 目录中,文件名为 <command>。而如果是全局模式安装,则会在 npm 全局安装路径的 bin 目录下创建指向 <file> 名为 <command> 的软链。因此,./node_modules/.bin/webpack 文件在通过命令行调用时,实际上就是在执行 node ./node_modules/.bin/webpack.js 命令。

正如上一节所说,npm run 命令在执行时会把 ./node_modules/.bin 加入到 PATH 中,使我们可直接调用所有提供了命令行调用接口的依赖包。所以这里就引出了一个最佳实践:

将项目依赖的命令行工具安装到项目依赖文件夹中,然后通过 npm scripts 调用;而非全局安装

举例而言 webpack 作为前端工程标配的构建工具,虽然我们都习惯了全局安装并直接使用命令行调用,但不同的项目依赖的 webpack 版本可能不同,相应的 webpack.config.js 配置文件也可能只兼容了特定版本的 webpack. 如果我们仅全局安装了最新的 webpack 4.x 并使用 webpack 命令调用,在一个依赖 webpack 3.x 的工程中就会无法成功执行构建。

但如果这类工具总是本地安装,我们要调用一个命令,要手动添加 ./node_modules/.bin 这个长长的前缀,未免也太麻烦了,我们 nodejs 开发者都很懒的。于是 npm 从5.2 开始自带了一个新的工具 npx.

5.3 npx

npx 的使用很简单,就是执行 npx <command> 即可,这里的 <command> 默认就是 ./node_modules 目录中安装的可执行脚本名。例如上面本地安装好的 webpack 包,我们可以直接使用 npx webpack 执行即可。

除了这种最简单的场景, npm cli 团队开发者 Kat Marchán 还在这篇文章中介绍了其他几种 npx 的神奇用法: Introducing npx: an npm package runner, 国内有位开发者 robin.law 将原文翻译为中文 npx是什么,为什么需要npx?.

有兴趣的可以戳链接了解,懒得点链接的,看总结:

场景a) 一键执行远程 npm 源的二进制包

除了在 package 中执行 ./node_modules/.bin 中已安装的命令, 还可以直接指定未安装的二进制包名执行。例如我们在一个没有 package.json 也没有 node_modules 的目录下,执行:

npx cowsay hello

npx 将会从 npm 源下载 cowsay 这个包(但并不安装)并执行:

 _______ 
< hello >
 ------- 
        \   ^__^
         \  (oo)\_______
            (__)\       )\/\
                ||----w |
                ||     ||

这种用途非常适合 1. 在本地简单测试或调试 npm 源上这些二进制包的功能;2. 调用 create-react-app 或 yeoman 这类往往每个项目只需要使用一次的脚手架工具

PS: 此处有彩蛋,执行这条命令试试:

npx workin-hard

场景b) 一键执行 GitHub Gist

还记得前面提到的 2.1 package定义 么,npm install <package> 可以是包含了有效 package.json 的 git url.

刚好 GitHub Gist 也是 git 仓库 的一种,集合 npx 就可以方便地将简单的脚本共享给其他人,拥有该链接的人无需将脚本安装到本地工作目录即可执行。将 package.json 和 需执行的二进制脚本上传至 gist, 在运行 npx <gist url> 就可以方便地执行该 gist 定义的命令。

原文作者 Kat Marchán 提供了这个示例 gist, 执行:

npx https://gist.github.com/zkat/4bc19503fe9e9309e2bfaa2c58074d32

可得到一个来自 GitHubGist 的 hello world 问候。

场景c) 使用不同版本 node 执行命令

将 npx 与 Aria Stewart 创建的 node 包 (https://www.npmjs.com/package... 结合,可以实现在一行命令中使用指定版本的 node 执行命令。

例如先后执行:

npx node@4 -e "console.log(process.version)"
npx node@6 -e "console.log(process.version)"

将分别输出 v4.8.7v6.13.0.

往常这种工作是由 nvm 这类 node 版本管理工具来做的,但 npx node@4 这种方式免去 nvm 手动切换配置的步骤,更加简洁简单。

6. npm 配置

6.1 npm config

npm cli 提供了 npm config 命令进行 npm 相关配置,通过 npm config ls -l 可查看 npm 的所有配置,包括默认配置。npm 文档页为每个配置项提供了详细的说明 https://docs.npmjs.com/misc/c... .

修改配置的命令为 npm config set <key> <value>, 我们使用相关的常见重要配置:

  • proxy, https-proxy: 指定 npm 使用的代理
  • registry 指定 npm 下载安装包时的源,默认为 https://registry.npmjs.org/ 可以指定为私有 Registry 源
  • package-lock 指定是否默认生成 package-lock 文件,建议保持默认 true
  • save true/false 指定是否在 npm install 后保存包为 dependencies, npm 5 起默认为 true

删除指定的配置项命令为 npm config delete <key>.

6.2 npmrc 文件

除了使用 CLI 的 npm config 命令显示更改 npm 配置,还可以通过 npmrc 文件直接修改配置。

这样的 npmrc 文件优先级由高到低包括:

  • 工程内配置文件: /path/to/my/project/.npmrc
  • 用户级配置文件: ~/.npmrc
  • 全局配置文件: $PREFIX/etc/npmrc (即npm config get globalconfig 输出的路径)
  • npm内置配置文件: /path/to/npm/npmrc

通过这个机制,我们可以方便地在工程跟目录创建一个 .npmrc 文件来共享需要在团队间共享的 npm 运行相关配置。比如如果我们在公司内网环境下需通过代理才可访问 registry.npmjs.org 源,或需访问内网的 registry, 就可以在工作项目下新增 .npmrc 文件并提交代码库。

proxy = http://proxy.example.com/
https-proxy = http://proxy.example.com/
registry = http://registry.example.com/

因为项目级 .npmrc 文件的作用域只在本项目下,所以在非本目录下,这些配置并不生效。对于使用笔记本工作的开发者,可以很好地隔离公司的工作项目、在家学习研究项目两种不同的环境。

将这个功能与 ~/.npm-init.js 配置相结合,可以将特定配置的 .npmrc 跟 .gitignore, README 之类文件一起做到 npm init 脚手架中,进一步减少手动配置。

6.3 node 版本约束

虽然一个项目的团队都共享了相同的代码,但每个人的开发机器可能安装了不同的 node 版本,此外服务器端的也可能与本地开发机不一致。

这又是一个可能带来不一致性的因素 —— 但也不是很难解决,声明式约束+脚本限制即可。

声明:通过 package.jsonengines 属性声明应用运行所需的版本运行时要求。例如我们的项目中使用了 async, await 特性,查阅兼容性表格得知最低支持版本为 7.6.0,因此指定 engines 配置为:

{
    "engines": { "node": ">=7.6.0"}
}

强约束(可选):在 npm 中以上字段内容仅作为建议字段使用,若要在私有项目中添加强约束,需要自己写脚本钩子,读取并解析 engines 字段的 semver range 并与运行时环境做对比校验并适当提醒。

7. 小结 npm 最佳实践

  • 使用 npm-init 初始化新项目
  • 统一项目配置: 需团队共享的 npm config 配置项,固化到 .npmrc 文件中
  • 统一运行环境,统一 package.json,统一 package-lock 文件
  • 合理使用多样化的源安装依赖包: npm install <git url>|<local file>
  • 使用 npm: >=5.2 版本
  • 使用 npm scripts 与 npx (npm: >=5.2) 脚本管理应用相关脚本

8. 更多资料

参考

文档

延伸阅读

查看原文

shinn_lancelot 收藏了文章 · 2018-11-28

移动端上传图片翻转的解决方案

最近在处理移动端选择图片实时预览并上传时遇到一个问题:上传前图片预览正常,但上传到服务器上的图片展示到页面上时,有时会出现图片翻转的问题,一般是翻转 90 度。后经一翻研究找到了问题所在,特在此记录一下。

问题描述

接上面提到的问题,经过一些测试,发现:如果选取的图片是在横屏状态下拍摄的,则上传后不会出现图片翻转的问题;反之,如果是在竖屏状态下拍摄的,上传后就会出现图片翻转的问题。

问题分析

首先,图片在本地预览时正常,而且前端最后提交到后端的数据是用 FormData 来封装处理的,用 FormData 可以保证提交的数据和通常的表单提交没有什么区别,对后端来说是透明的,后端的处理逻辑不用修改就可以处理带文件的 Ajax 提交。因此,我猜想问题可能出在了后端。

但是后端也没有做什么特殊的处理,怎么展示的时候图片就翻转了呢?

OK,前面提到过这个问题和手机的拍照模式有关,竖屏下拍的照片会重现这个问题,横屏下的则不会。难道这两种模式下拍摄的照片不一样?照片中保存的数据不一样?,这不由的使我想到了 [Exif][] 这个名词,虽然我对它不是十分了解,但是印象中它是用来保存照片的一些元数据信息的,也许和它有关。

简单的一搜索发现:Exif[orientation]有一篇关于它的详细的介绍,此处不展开细讲。简单来说就是它有 8 个值,用来表示照片拍摄时相机旋转的状态,而且我们可以通过编程的方式读取它。我们平常用电脑来查看手机拍摄的照片时也会偶尔出现图片翻转的情况,其原因也是照片查看软件没有根据这个值将照片自动翻转。

经过搜索,在 SO 上找到了相关的问题。其思路就是读取照片的 Exif 信息,判断 Orientation 的值,然后将图片进行相应的旋转。问题已确定,后端同学很快就将这个问题给修复了,并且添加到了他们的公共模块中。

你以为问题就这样解决了,其时并没有。

后续

过了几天,公司购置了几台全新的测试机,测试同学将系统在一台三星的机子上一测,又发现新问题了:选择完图片进行本地预览时,发现图片翻转了!但上传后再展示又是正常的。WTF!

这次没得说了,问题肯定出在了前端预览上。但是之前在安卓和 iOS 的设备上测试都是正常的呀,难道和机型有关?问了一下测试,得知这台三星用的是 Android 5.0 的系统,好先进有木有!这难道是 5.0 中引入的一个问题?别想这些了,反正我们也不可能在系统层面去修复这个问题,还是想想如何去兼容 5.0 吧。

当时想到了两种方法,一种是选择图片后先上传到服务器,再展示上传的图片进行预览。这种方法的优点是兼容性好,前端处理相对简单;缺点是后端要提供图片上传的接口,并且如果用户在保存之前更换图片并不会删除之前上传的图片。移动端的微博配图就是用的这种方法。

另一种方法是在本地进行图片翻转。我们可以使用 FileReader API 来读取图片,之后像后端那样分析 Exif 信息,旋转图片。这种做法的优点是,预览操作完全在本地完成,不会产生不必要的文件上传。缺点是 FileReader 在 Android 4.1 及以上的机型才可以使用,加上前端处理文件数据可能稍显复杂,另外性能也是个问题。

但是最终我们选择了第二种方案。原因是我们之前的照片预览就是用的 FileReader API,不需要考虑低版本安卓的兼容性,这和项目的实际情况有关。另一个原因是我们认为也许有前人已经遇到过这种问题,也许已经有了比较好的处理这个问题的功能模块。后经搜索,找到了一个 fix-orientation 模块,可以通过 npm i fix-orientation --save 使用,具体的用法可以参考项目的文档。后来,扫了一眼它的代码,发现它是用 canvas 来实现图片的旋转,并不会对正常的图片进操作,性能问题不大,iOS 上秒开,安卓设备上稍长,可以添加 loading 效果缓解一下。

至此,问题完美解决。

另外,像 fix-orientation 这样小而美的模块有很多,大家平时可以多关注一下。

原文链接:http://happycoder.net/fix-image-upload-rotation/

查看原文

shinn_lancelot 收藏了文章 · 2018-11-28

基于html2canvas实现网页保存为图片及图片清晰度优化

2019.12 更新:《高质量前端快照方案:来自页面的「自拍」
2019.12 更新:《高质量前端快照方案:来自页面的「自拍」
2019.12 更新:《高质量前端快照方案:来自页面的「自拍」

新作见上面哦~ 👆👆👆

本次技术调研来源于H5项目中的一个重要功能需求:实现微信长按网页保存为截图

这里有个栗子(请用微信打开,长按图片即可保存):3分钟探索你的知识边界

将整个网页保存为图片是一个十分有趣的功能,常见于H5活动页的结尾页分享。以下则是项目中调研和踩坑的一些小结和汇总。


一、实现HTML页面保存为图片

1.1 已知可行方案

现有已知能够实现网页保存为图片的方案包括:

  • 方案1:将DOM改写为canvas,然后利用canvas的toDataURL方法实现将DOM输出为包含图片展示的data URI
  • 方案2:使用html2canvas.js实现(可选搭配Canvas2Image.js实现网页保存为图片)
  • 方案3:使用rasterizeHTML.js实现

1.2 解决方案的选择

  • 方案1:需要手动计算每个DOM元素的Computed Style,然后需要计算好元素在canvas的大小位置等属性。

    方案1难点

    1. 相当于完全重写了整个页面的布局样式,增加了工作量。
    2. 由于canvas中没有的对象概念,对于元素丰富、布局复杂的页面,不易重构。
    3. 所有DOM元素改写进canvas会带来一些困难,例如:难以支持响应式,图片元素清晰度不佳和文字点击区域识别问题等。
  • 方案2:该类功能中Github上stars最多(至今仍在维护),Stack Overflow亦有丰富的讨论。只需简单调用html2canvas方法并设定配置项即可。
  • 方案3:该方案的限制较多,目前仅支持3类可转为canvas的目标格式: 页面url,html字符串和document对象。

小结: html2canvas是目前实现网页保存为图片功能的综合最佳选择。

1.3 html2canvas的使用方法

官方GitHub:https://github.com/niklasvh/h...

以下描述针对html2canvas版本是0.5.0-beta4

1.3.1 实现保存为图片的第一步:html转为canvas

基于html2canvas.js可将一个元素渲染为canvas,只需要简单的调用html2canvas(element[, options]);即可。下列html2canvas方法会返回一个包含有<canvas>元素的promise

html2canvas(document.body).then(function(canvas) {
    document.body.appendChild(canvas);
});

1.3.2 实现保存为图片的第二步:canvas转image

上一步生成的canvas即为包含目标元素的<canvas>元素对象。实现保存图片的目标只需要将canvas转image即可。

这里的转换方案有2种

  • 方案1:基于原生canvas的toDataURL方法将canvas输出为data: URI类型的图片地址,再将该图片地址赋值给<image>元素的src属性即可
  • 方案2:使用第三方库Canvas2Image.js,调用其convertToImage方法即可(GitHub

实际上,Canvas2Image.js也是基于canvas.toDataURL的封装,相比原生的canvas API对于转为图片的功能上考虑更为具体(未压缩的包大小为7.4KB),适合项目使用。

二、生成图片的清晰度优化方案

2.1 基础的清晰度优化方案

最终图片的清晰度取决于第一步中html转换成的canvas的清晰度。

现有解决方案参考;

基本原理为:
canvas的属性widthheight属性放大为2倍(或者设置为devicePixelRatio倍),最后将canvas的CSS样式width和height设置为原先1倍的大小。

例如:希望在html中实际显示的<canvas>宽高分别为160px,90px则可作如下设置

<canvas width="320" height="180" style="width:160px;height:90px;"></canvas>

参考上述文档具体的使用案例如下;

convert2canvas() {

    var shareContent = YourTargetElem; 
    var width = shareContent.offsetWidth; 
    var height = shareContent.offsetHeight; 
    var canvas = document.createElement("canvas"); 
    var scale = 2; 

    canvas.width = width * scale; 
    canvas.height = height * scale; 
    canvas.getContext("2d").scale(scale, scale); 

    var opts = {
        scale: scale, 
        canvas: canvas, 
        logging: true, 
        width: width, 
        height: height 
    };
    html2canvas(shareContent, opts).then(function (canvas) {
        var context = canvas.getContext('2d');

        var img = Canvas2Image.convertToImage(canvas, canvas.width, canvas.height);

        document.body.appendChild(img);
        $(img).css({
            "width": canvas.width / 2 + "px",
            "height": canvas.height / 2 + "px",
        })
    });
}

2.2 进阶的清晰度优化方案

上述设置可以解决通常情况下图片不清晰的问题,不过探索并没有结束。

实际在我们的项目中,即使作出2.1节的设置后,大果粒一般的渲染结果依然尴尬。

下面直接给出3条进一步的优化策略:

  1. 更改百分比布局px布局(如果原先是百分比布局的话)
  2. 关闭canvas默认的抗锯齿设
  3. 设置模糊元素的widthheight为素材原有宽高,然后通过transform: scale进行缩放。这里scale的数值由具体需求决定。
基本原理
  1. 如果原来使用百分比设置元素宽高,请更改为px为单位的宽高,避免样式二次计算导致的模糊
  2. 默认情况下,canvas的抗锯齿是开启的,需要关闭抗锯齿来实现图像的锐化(MDN: imageSmoothingEnabled )
  3. 除了canvas可以通过扩大2倍宽高然后缩放至原有宽高来提高清晰度,对于DOM中其他的元素也可以使用css样式scale来实现同样的缩放
例: html2canvas配置
convert2canvas() {

    var cntElem = $('#j-sec-end')[0];

    var shareContent = cntElem;//需要截图的包裹的(原生的)DOM 对象
    var width = shareContent.offsetWidth; //获取dom 宽度
    var height = shareContent.offsetHeight; //获取dom 高度
    var canvas = document.createElement("canvas"); //创建一个canvas节点
    var scale = 2; //定义任意放大倍数 支持小数
    canvas.width = width * scale; //定义canvas 宽度 * 缩放
    canvas.height = height * scale; //定义canvas高度 *缩放
    canvas.getContext("2d").scale(scale, scale); //获取context,设置scale 
    var opts = {
        scale: scale, // 添加的scale 参数
        canvas: canvas, //自定义 canvas
        // logging: true, //日志开关,便于查看html2canvas的内部执行流程
        width: width, //dom 原始宽度
        height: height,
        useCORS: true // 【重要】开启跨域配置
    };

    html2canvas(shareContent, opts).then(function (canvas) {

        var context = canvas.getContext('2d');
        // 【重要】关闭抗锯齿
        context.mozImageSmoothingEnabled = false;
        context.webkitImageSmoothingEnabled = false;
        context.msImageSmoothingEnabled = false;
        context.imageSmoothingEnabled = false;
        
        // 【重要】默认转化的格式为png,也可设置为其他格式
        var img = Canvas2Image.convertToJPEG(canvas, canvas.width, canvas.height);

        document.body.appendChild(img);

        $(img).css({
            "width": canvas.width / 2 + "px",
            "height": canvas.height / 2 + "px",
        }).addClass('f-full');

    });
}
例: DOM元素样式:

.targetElem {width: 54px;height: 142px;margin-top:2px;margin-left:17px;transform: scale(0.5)}

三、含有跨域图片的配置

由于canvas对于图片资源的同源限制,如果画布中包含跨域的图片资源则会污染画布,造成生成图片样式混乱或者html2canvas方法不执行等问题。

以下主要解决两类跨域的图片资源:包括已配置过CORS的CDN中的图片资源和微信用户头像图片资源。

3.1 针对CDN中的图片的配置

  1. 要求CDN的图片配置好CORSCDN配置好后,通过chrome开发者工具可以看到响应头中应含有Access-Control-Allow-Origin的字段。
  2. 开启html2canvasuseCORS配置项。即作如下设置:

var opts = {useCORS: true};

html2canvas(element, opts);

注意
如果没有开启html2canvasuseCORS配置项,html2canvas会正常执行且不会报错,但是不会输出对应的CDN图片
(已测试同时包含CDN的图片本地图片的资源的页面,但是只有本地图片能够被正常渲染出来)

3.2 针对微信用户头像的配置

如果需要将微信平台中的用户头像一并保存为图片,3.1的方案无能为力。可通过配置服务端代理转发(forward)实现,此处不赘述。

其他注意事项

1. margin的遮挡问题

微信中,唤出长按保存图片的菜单要求长按的对象直接是<image>元素,如果<image>元素上方存在遮挡,则不会唤出菜单。
而事实上,引发遮挡的并不只是非<image>元素,还可能是margin属性。例如:若在页面底部,对一个绝对定位的元素设置了数值很大的margin-top,则margin-top所涉及的区域,均无法长按唤出菜单。解决方案:将margin-top改用为top即可。

2. 安卓版微信保存图片失败的问题

canvas2img默认保存图片的格式为png,而在安卓版微信中所生成的图片尽管能长按唤出保存图片的菜单,但是无法正确保存到本地相册解决方案:设置canvas2img的生成图片格式配置项为jpeg即可。

3. JPEG的黑屏问题

设置canvas2img输出格式为jpeg,会有一定几率导致生成的图片包含大量的黑色块。可能的解决方案:缩减部分图片元素的体积和尺寸大小。

4. 不能保留动效

在图片的转化前,必须停止或者删除动效后才能正确渲染出图片,否则生成的图片是破裂的。

参考文献

查看原文

shinn_lancelot 赞了文章 · 2018-11-27

通过 Certbot 安装 Let's Encrypt 证书,来实现全站的 HTTPS 访问

相关知识

HTTP/HTTPS 是什么?

简单来说,HTTP 是一个传输网页内容的协议,比如我们浏览一个网页,网页上的文字、图片、 CSS 、 JS 等文件都是通过 HTTP 协议传输到我们的浏览器,然后被我们看到。因为 HTTP 是明文传输的,通过 HTTP 协议传输的内容很容易被偷看和篡改,为了安全(你肯定不想被人偷看或者篡改网页内容吧,比如网站银行密码什么的。)就为 HTTP 协议再加上了一层 SSL/TLS 安全协议,所以就有了 HTTPS 。

SSL/TLS 是什么?

SSL 是指安全套接字层(Secure Sockets Layer),内心纯洁的同学也可以理解为「带安全套的 HTTP」,因为带了安全套,所以当然会比较安全。TLS 是 传输层安全协议(Transport Layer Security),SSL 和 TLS 是同一个东西的不同阶段,理解为同一个东西也行,都是安全协议就对了。

为什么要部署 HTTPS?

说到底,就是 HTTPS 更安全。甚至为了安全,一个专业可靠的网站, HTTPS 是必须的。 Firefox 和 Chrome 都计划将没有配置 SSL 加密的 HTTP 网站标记为不安全,目前它们也正在联合其他相关的基金会与公司推动整个互联网 HTTPS 化,现在大家访问的一些主要的网站。如 Google 多年前就已经全部启用 HTTPS ,国内的淘宝、搜狗、知乎、百度等等也全面 HTTPS 了。甚至 Google 和百度的搜索结果也正在给予 HTTPS 的网站更高的排名和优先收录权。

怎么部署 HTTPS 呢?

你只需要有一张被信任的 CA ( Certificate Authority )也就是证书授权中心颁发的 SSL 安全证书,并且将它部署到你的网站服务器上。一旦部署成功后,当用户访问你的网站时,浏览器会在显示的网址前加一把小绿锁,表明这个网站是安全的,当然同时你也会看到网址前的前缀变成了 HTTPS ,不再是 HTTP 了。

怎么获得 SSL 安全证书呢?

理论上,我们自己也可以签发 SSL 安全证书,但是我们自己签发的安全证书不会被主流的浏览器信任,所以我们需要被信任的证书授权中心( CA )签发的安全证书。而一般的 SSL 安全证书签发服务都比较贵,比如 Godaddy 、 GlobalSign 等机构签发的证书一般都需要20美金一年甚至更贵,不过为了加快推广 HTTPS 的普及, EEF 电子前哨基金会、 Mozilla 基金会和美国密歇根大学成立了一个公益组织叫 ISRG ( Internet Security Research Group ),这个组织从 2015 年开始推出了 Let’s Encrypt 免费证书。这个免费证书不仅免费,而且还相当好用,所以我们就可以利用 Let’s Encrypt 提供的免费证书部署 HTTPS 了。

Let’s Encrypt 简介

前面已经介绍过, Let’s Encrypt 是 一个叫 ISRG ( Internet Security Research Group ,互联网安全研究小组)的组织推出的免费安全证书计划。参与这个计划的组织和公司可以说是互联网顶顶重要的先驱,除了前文提到的三个牛气哄哄的发起单位外,后来又有思科(全球网络设备制造商执牛耳者)、 Akamai 加入,甚至连 Linux 基金会也加入了合作,这些大牌组织的加入保证了这个项目的可信度和可持续性。

Certbot 简介

ISRG 的发起者 EFF (电子前哨基金会)为 Let’s Encrypt 项目发布了一个官方的客户端 Certbot ,利用它可以完全自动化的获取、部署和更新安全证书。虽然第三方工具也可以使用,但是官方工具更权威,风险也更小,而且遇到问题也更容易解决,毕竟有官方的支持。

实际操作

Certbot 使用方法

Certbot 的官网是https://certbot.eff.org/, 我们打开这个链接,选择自己使用的 web server 和操作系统。选择完之后,官网就会显示出对应的安装操作步骤。
以目前我所使用的服务器为例,web server 使用Nginx(1.12),操作系统是CentOS(7.3)。因为 Certbot 打包在EPEL中,所以在安装 Certbot 之前要先安装EPEL

yum -y install epel-release

然后按着官网给出的步骤提示命令安装 Certbot

yum install python2-certbot-nginx

安装完毕后,继续输入官网提示命令

certbot --nginx

运行此命令会自动获取证书,并且 Certbot 会自动编辑Nginx配置文件配置HTTPS服务。

这边在运行配置命令时遇到一个坑
ImportError: No module named 'requests.packages.urllib3'

可以看出是缺少一个模块包,看配置命令在命令台的输出,Certbot 是用 python 来写 Nginx 配置。既然是用的 python,可以通过 pip list 命令查看 python 的依赖包列表。但是查看是已经存在的,这个问题很快通过 Certbot 在 github 上的一个 Issues 找到了答案(Issues链接)。目前看是包的版本依赖有问题,执行以下命令进行依赖包的重装

pip uninstall requests
pip uninstall urllib3
yum remove python-urllib3
yum remove python-requests
yum install python-urllib3
yum install python-requests
yum install certbot

再次执行 certbot --nginx 命令成功。命令执行后首先会提示输入一个邮箱地址,主要作用是订阅一些通知,输入后回车。

[root@izuf6fco0zwyipdq9485s2z ~]# certbot --nginx
Saving debug log to /var/log/letsencrypt/letsencrypt.log
Plugins selected: Authenticator nginx, Installer nginx

▽
Enter email address (used for urgent renewal and security notices) (Enter 'c' to
cancel): demo@mail.com // 演示邮箱
Starting new HTTPS connection (1): acme-v02.api.letsencrypt.org

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

回车后 Certbot 就会自动请求下载 Let's Encrypt 证书,并设置 Nginx 配置文件。期间会有一些同意相关协议和选择哪个网站配置的交互,这个看输出文字就能明白意思。

...
▽
server {
(A)gree/(C)ancel: a

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Would you be willing to share your email address with the Electronic Frontier
Foundation, a founding partner of the Let's Encrypt project and the non-profit
organization that develops Certbot? We'd like to send you email about our work
encrypting the web, EFF news, campaigns, and ways to support digital freedom.
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
(Y)es/(N)o: y
Starting new HTTPS connection (1): supporters.eff.org

Which names would you like to activate HTTPS for?
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
1: kisstime.top
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Select the appropriate numbers separated by commas and/or spaces, or leave input
blank to select all options shown (Enter 'c' to cancel): 1
Obtaining a new certificate
Performing the following challenges:
http-01 challenge for kisstime.top
Cleaning up challenges
...
但是这边到 Certbot 设置 Nginx 配置文件的时候遇到一个坑,报如下一个错误
An unexpected error occurred:
UnicodeDecodeError: 'ascii' codec can't decode byte 0xe5 in position 2: ordinal not in range(128)
Please see the logfiles in /var/log/letsencrypt for more details.

熟悉 python 的同学应该对这个错误比较了解。不过博主 python 还是用的比较少,花了点时间了解了一下。

在解决错误之前,首先要了解unicode和utf-8的区别。unicode指的是万国码,是一种“字码表”。而utf-8是这种字码表储存的编码方法。unicode不一定要由utf-8这种方式编成bytecode储存,也可以使用utf-16,utf-7等其他方式。目前大多都以utf-8的方式来变成bytecode。其次,Python中字符串类型分为byte string 和 unicode string两种。如果在python文件中指定编码方式为utf-8(#coding=utf-8),那么所有带中文的字符串都会被认为是utf-8编码的byte string(例如:mystr="你好"),但是在函数中所产生的字符串则被认为是unicode string。问题就出在这边,unicode string 和 byte string 是不可以混合使用的,一旦混合使用了,就会产生这样的错误。例如:
self.response.out.write("你好"+self.request.get("argu"))
其中,"你好"被认为是byte string,而self.request.get("argu")的返回值被认为是unicode string。由于预设的解码器是ascii,所以就不能识别中文byte string。然后就报错了。

那理解了这个错误原因后,我这边首先想到的就是网站的 Nginx 配置文件中是否含有中文。打开一看,确实存在中文注释。将注释去掉,重新执行命令成功不在报错

接着命令交互会提示是否将所有HTTP重定向到HTTPS,我这边选择全部重定向也就是「2」。

Cleaning up challenges
Deploying Certificate to VirtualHost:  //这边会显示你的网站配置文件目录

Please choose whether or not to redirect HTTP traffic to HTTPS, removing HTTP access.
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
1: No redirect - Make no further changes to the webserver configuration.
2: Redirect - Make all requests redirect to secure HTTPS access. Choose this for
new sites, or if you're confident your site works on HTTPS. You can undo this
change by editing your web server's configuration.
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Select the appropriate number [1-2] then [enter] (press 'c' to cancel): 2
Redirecting all traffic on port 80 to ssl in //这边会显示你的网站配置文件目录

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Congratulations! You have successfully enabled https://kisstime.top

You should test your configuration at:
https://www.ssllabs.com/ssltest/analyze.html?d=kisstime.top
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

IMPORTANT NOTES:
 - Congratulations! Your certificate and chain have been saved at:
   .../fullchain.pem
   Your key file has been saved at:
   .../privkey.pem
   Your cert will expire on 2019-02-14. To obtain a new or tweaked
   version of this certificate in the future, simply run certbot again
   with the "certonly" option. To non-interactively renew *all* of
   your certificates, run "certbot renew"
 - If you like Certbot, please consider supporting our work by:

   Donating to ISRG / Let's Encrypt:   https://letsencrypt.org/donate
   Donating to EFF:                    https://eff.org/donate-le

OK,当完成上面最后一步后,输入如上信息后就表明 HTTPS 配置成功了!我们可以通过提示中的 SSL Server Test 网址来测试网站是否能够 HTTPS 来进行访问

You should test your configuration at:
https://www.ssllabs.com/ssltest/analyze.html?d=kisstime.top

事实证明,高兴的太早了。。。通过测试网站和直接访问域名发现网站都是访问不了。思考了一下,确认下服务器的443端口是否开启,我这边用的是阿里云的服务器,登录控制台查看安全组规则,发现确实是443端口没有开启,配置开启后,重新访问成功。

其他

Nginx的设置说明

基本上 Certbot 会在对应的 Nginx 配置文件加上下面的参数:

server {
    # ... other configs
    
    # SSL 设置
    listen 443 ssl;

    # set crt and key
    ssl_certificate .../fullchain.pem;
    ssl_certificate_key .../privkey.pem;
    
    # include 基本的 ssl 设置
    include .../options-ssl-nginx.conf;
    
    # Certbot 也会生成一把 Diffie-Hellman 密钥
    ssl_dhparam .../ssl-dhparams.pem;

    # ... other configs
}

自动更新证书

Let’s Encrypt 免费SSL证书用起来非常方便,但每次申请只有三个月有效期,在每次到期之前都需要重新申请,Certbot 已经提供了一键续订的命令

certbot renew

我们可以通过添加此条命令的cron作业(或systemd计时器)来安排证书的自动续订

SHELL=/bin/bash
PATH=/sbin:/bin:/usr/sbin:/usr/bin

0 0,12 * * * certbot renew --quiet 

Certbot 的 log 路径

Certbot 的 log 预设路径在 /var/log/letsencrypt,有需要可以去确认查看。

相关第三方

certbot-auto是对脚本certbot的封装,可以设置系统环境或自动升级。

参考资料

牛顿曾经说过:如果说我看得比别人更远些,那是因为我站在巨人的肩膀上。(If I have seen further, it is by standing on the shoulders of giants.)感谢。

博客原文链接

原文链接

查看原文

赞 16 收藏 11 评论 0