senntyou

senntyou 查看完整档案

深圳编辑哈尔滨工业大学  |  电子科学与技术 编辑深圳市妙严科技有限公司  |  web前端工程师 编辑 www.senntyou.com/ 编辑
编辑

达则兼济天下,穷则独善其身。

个人动态

senntyou 赞了回答 · 5月27日

html5 canvas绘制图片模糊的问题

用了@KevinYue 的例子,感觉不是很好使,我现在的解决方案,不管当前的devicePixelRatio的值是多少,统一将canvasDOM节点的width属性设置为其csswidth属性的两倍,同理将height属性也设置为cssheight属性的两倍,即:

<canvas width="320" height="180" style="width:160px;height:90px;"></canvas>

这样整个canvas的坐标系范围就扩大为两倍,但是在浏览器的显示大小没有变,canvas画图的时候,按照扩大化的坐标系来显示,不清晰的问题就得以改善了。

关注 25 回答 4

senntyou 赞了回答 · 5月27日

解决为什么在canvas上画图片比设置的尺寸大很多?

<canvas id='share' canvas-id='share' width="750" height="1334" style='width:750px;height:1334px;'>
</canvas>

canvas 的大小不是通过 style 设置的,这两个宽高是两码事。

关注 2 回答 2

senntyou 发布了文章 · 5月25日

解决 uni-app 微信小程序项目中腾讯统计 mta 不上报数据的问题

解决 uni-app 微信小程序项目中腾讯统计 mta 不上报数据的问题

uni-app 微信小程序项目开发中,发现腾讯统计 mta 不上报数据。

1. 原因

  1. uni-app 框架与 mta 组件都对原生的 Page 对象进行了重写,在 onLoad 生命周期函数中上报数据,这一点开发者无感知
  2. 因为 uni-app 框架会首先加载自身框架脚本,导致 mta 后加载的脚本对 Page 对象重写无效(两者冲突)
  3. 所以,mta 组件中设置了 "autoReport": true 会导致数据不上报("autoReport": false 配置不受影响,因为不需要重写 Page

uni-app 框架脚本重写 Page:

var MPPage = Page;

Page = function Page() {var options = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
  initHook('onLoad', options);
  return MPPage(options);
};

mta-wechat-analysis.js 重写 Page:

function initOnload() {
  var a = Page;
  Page = function (b) {
    var c = b.onLoad;
    b.onLoad = function (a) {
      c && c.call(this, a);
      MTA.Data.lastPageQuery = MTA.Data.pageQuery;
      MTA.Data.pageQuery = a;
      MTA.Data.lastPageUrl = MTA.Data.pageUrl;
      MTA.Data.pageUrl = getPagePath();
      MTA.Data.show = !1;
      MTA.Page.init()
    };
    a(b)
  }
}

按理说,前后两次对 Page 进行重写,应该是不冲突、都有效的,但 uni-app 在对 Vue 组件转微信小程序原生组件时,使用了局部封装的函数,导致后面其他脚本对 Page 的重写无效

function createPage(vuePageOptions) {
  return Component(parsePage(vuePageOptions));
}

2. 解决

有两个解决方案:

  1. mta-wechat-analysis.js 脚本放到 uni-app 框架脚本之前加载,但官方并没有提供这个功能,所以放弃这个方法
  2. 重写 Vue 组件,在 Vue 组件里上报统计数据

因为 Vue 组件是不能重写生命周期函数的,所以只能重写调用生命周期函数的方法 Vue.prototype.__call_hook

mta-wechat-analysis.js:

- function initOnload() {
-   var a = Page;
-   Page = function (b) {
-     var c = b.onLoad;
-     b.onLoad = function (a) {
-       c && c.call(this, a);
-       MTA.Data.lastPageQuery = MTA.Data.pageQuery;
-       MTA.Data.pageQuery = a;
-       MTA.Data.lastPageUrl = MTA.Data.pageUrl;
-       MTA.Data.pageUrl = getPagePath();
-       MTA.Data.show = !1;
-       MTA.Page.init()
-     };
-     a(b)
-   }
- }

+ import Vue from 'vue';
+
+ function initOnload() {
+   // 重写 Vue.prototype.__call_hook 方法
+   Vue.prototype.__call_hook_proxy = Vue.prototype.__call_hook;
+   Vue.prototype.__call_hook = function(hook, args) {
+     if (hook === 'onLoad') {
+       MTA.Data.lastPageQuery = MTA.Data.pageQuery;
+       MTA.Data.pageQuery = args;
+       MTA.Data.lastPageUrl = MTA.Data.pageUrl;
+       MTA.Data.pageUrl = getPagePath();
+       MTA.Data.show = !1;
+       MTA.Page.init();
+     }
+     return this.__call_hook_proxy(hook, args);
+   };
+ }

改写后的 mta-wechat-analysis.js 脚本可以点这里下载

后续

更多博客,查看 https://github.com/senntyou/blogs

作者:深予之 (@senntyou)

版权声明:自由转载-非商用-非衍生-保持署名(创意共享 3.0 许可证

查看原文

赞 4 收藏 2 评论 0

senntyou 发布了文章 · 2019-10-22

IOS 浏览器页面布局错位(如:点不到)的分析与解决

IOS 浏览器页面布局错位(如:点不到)的分析与解决

IOS 浏览器软键盘的拉起与收缩、微信 IOS 浏览器底部导航条的显示与隐藏,很容易导致页面布局错位(相对窗体的绝对定位元素):

  • 明明按钮在这里,却要在上面一点儿点击屏幕才能点到它
  • 明明弹框是居中显示的,却向上偏移了很多,导致下面很多空白
  • 明明是固定浮动在某个位置,却点不到它

1. Android 与 IOS 的差异

  • 在 Android 中,软键盘的弹起与收缩会触发 window 对象的 resize 事件,而 IOS 不会
  • 微信 IOS 浏览器底部导航条的显示与隐藏会触发 window 对象的 resize 事件,而 Android 中没有底部导航条

2. IOS 里的一些特性

  • 为了达到极致的体验,IOS 浏览器很多特性是不遵循 W3C 规范的
  • 软键盘的弹起与收缩不会触发 window 对象的 resize 事件
  • 软键盘收缩后,固定定位的元素处于错位状态,需要滑动页面后才能刷新页面恢复到正常状态

3. 具体情况分析

不管是 IOS 浏览器软键盘的拉起与收缩,还是微信 IOS 浏览器底部导航条的显示与隐藏,都是改变的 window 窗体的大小。

微信 IOS 浏览器底部导航条的显示与隐藏跟软键盘的拉起与收缩是差不多的,但微信 IOS 浏览器底部导航条还有一个很大的特点:

在单页面应用(SPA)中,当路由发生变化时,底部导航条会一下子就显示,而这很难确定是先渲染了页面还是先显示了底部导航条,
这也很容易导致元素布局错位。

4. 怎么解决

4.1 监听键盘弹起与收缩,自动做一些操作

新建 watch-keyboard.js 脚本,引入到页面中。

当页面中键盘弹起时,body 会有 keyboard-active class,可以根据这个隐藏一些元素。

import {isIos} from '../utils';
import debounce from 'lodash/debounce';

// 初始高度
const winHeight = window.innerHeight;
// 判断是不是弹起了软键盘
const judgeDistance = 200;

if (!isIos) {
  window.addEventListener(
    'resize',
    debounce(() => {
      if (window.innerHeight < winHeight - judgeDistance) {
        // 键盘弹起
        document.body.classList.add('keyboard-active');
      } else {
        document.body.classList.remove('keyboard-active');
      }
    }, 300),
    !1
  );
}
else {
  // IOS 软键盘的弹起与收缩不会触发 `window` 对象的 `resize` 事件,用定时器实现

  // 保证能够滚动
  document.body.style.minHeight = (winHeight + 2) + 'px';
  // 上两次高度记录
  let secondLastWinHeight = winHeight;
  // 上一次高度记录
  let lastWinHeight = winHeight;

  setInterval(() => {
    const newWinHeight = window.innerHeight;

    // 变化结束
    if (secondLastWinHeight !== lastWinHeight && lastWinHeight === newWinHeight) {
      if (newWinHeight < winHeight - judgeDistance) {
        // 键盘弹起
        document.body.classList.add('keyboard-active');
      } else {
        document.body.classList.remove('keyboard-active');
        // window 需要滚动一下,让页面刷新一下,否则弹框会出现错位的问题
        window.scrollTo(0, window.scrollY ? window.scrollY - 1 : 1);
      }
    }

    secondLastWinHeight = lastWinHeight;
    lastWinHeight = newWinHeight;
  }, 300); // 可以根据需要调整间隔时间(越小越精确)
}

4.2 监听窗体大小变化,执行一个回调,做更多操作

当软键盘弹起时,又点击了一个按钮,然后显示弹框(如:从底部向上弹出)的时候,这个时候就需要等待软键盘收起之后,IOS 刷新屏幕之后,再显示弹框。

新建 wait-for-stable-win-height.js 脚本,引入到页面中。

import { isIos } from '../utils';

/**
 * 等待 window 高度不变了之后执行一个回调函数
 *
 * @param onComplete 完成的回调
 * @param delay 延迟多少时间再判断
 * @param interval 定时器间隔时间
 */
export default ({ onComplete, delay = 200, interval = 50 }) => {
  setTimeout(() => {
    let winHeight = window.innerHeight;
    const timer = setInterval(() => {
      const newWinHeight = window.innerHeight;

      if (winHeight === newWinHeight) {
        clearInterval(timer);
        if (onComplete) {
          if (!isIos) {
            setTimeout(() => {
              onComplete();
            }, 100);
            return;
          }

          // window 需要滚动一下,让页面刷新一下,否则弹框会出现错位的问题
          window.scrollTo(0, window.scrollY ? window.scrollY - 1 : 1);
          setTimeout(() => {
            onComplete();
          }, 200);
        }
      } else {
        winHeight = newWinHeight;
      }
    }, interval);
  }, delay);
};

后续

更多博客,查看 https://github.com/senntyou/blogs

作者:深予之 (@senntyou)

版权声明:自由转载-非商用-非衍生-保持署名(创意共享 3.0 许可证

查看原文

赞 16 收藏 14 评论 0

senntyou 发布了文章 · 2019-08-13

批量转换 AMD 与 CommonJs 模块到 ES 模块

批量转换 AMD 与 CommonJs 模块到 ES 模块

就前端而言,ES6 的模块化写法已经是主流了,但很多老的项目都是用 AMD 或者 CommonJs 规范写的。
由于 Webpack 现在已经慢慢不太支持 AMD 或者 CommonJs 规范的代码,所以需要把这两种规范的代码都转换成 ES6 规范的代码。
网上搜了一下,并没有这之类的工具,我就自己写了一个:conv-mod.

安装

npm install conv-mod -g

使用

conv-mod [options] <dir> [extraDirs...]

参数

  • -f, --filter <filter>: 查询某个字符串,过滤文件
  • -r, --regular: 当查询某个字符串,过滤文件时,把查询字符串当作正则匹配
  • --amd: 转化 AMD 模块
  • --cjs: 转化 CommonJs 模块

示例

转换 src 目录下的 AMD 与 CommonJs 代码

conv-mod src --amd --cjs

转换 src/dir1,src/dir2 目录下的 AMD 代码

conv-mod src/dir1 src/dir2 --amd

后续

更多博客,查看 https://github.com/senntyou/blogs

作者:深予之 (@senntyou)

版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证

查看原文

赞 6 收藏 4 评论 1

senntyou 发布了文章 · 2019-06-20

再谈前端项目的组件化

再谈前端项目的组件化

之前详细聊过的前端项目的组件化,可以参考 组件化私有 npm 仓库,今天来更进一步的说说前端项目的组件化。

1. 之前的组件化

目录结构:

-project1     # 项目1
-project2     # 项目2
-component1   # 组件1
-component2   # 组件2

project1package.json

{
  "dependencies": {
    "@yourCompany/component1": "^0.0.1",
    "@yourCompany/component2": "^0.0.1"
  }
}

在代码中使用:

import component1 from '@yourCompany/component1';

2. 之前的组件化方式存在的问题

  1. 更新组件比较麻烦,特别是对于一些与业务耦合比较深的组件,频繁更新会比较头疼
  2. 组件太多的时候,管理起来就感觉比较累,因为每个组件都是一个单独的项目,都有一套独立的构建环境
  3. 对于有些代码量小的组件,做一个单独的项目,实在有点大才小用

3. 另外的项目组件化方式

针对上面讲到的问题,另一种方式可以很好的解决:

目录结构:

-project1     # 项目1
-project2     # 项目2
-components   # 组件集合项目

components 组件集合项目的目录结构:

- src/             # 源代码目录
  - component1     # 组件1
  - component2     # 组件2
  - component3     # 组件3
  - ...

- package.json
- README.md
- CHANGELOG.md
- .eslintrc.js
- .stylelintrc.js
- .prettierrc.js
- ...

components 目录软链接 project1 目录下:

(注意: project1.gitignore 需加上 /components

# 以下是 linux 命令,windows 类似
cd project1
ln -s ../components ./

project1 项目的目录结构:

- src/             # 源代码目录
- components/      # 组件项目目录(软链接)

- package.json
- README.md
- CHANGELOG.md
- .eslintrc.js
- .stylelintrc.js
- .prettierrc.js
- ...

在代码中使用:

import component1 from 'relative/path/to/components/src/component1';

4. 两种方式的选择

上面的两种方式各有各的优势,可以配合一起使用。

大的、不常更新的组件可以使用 npm 包的方式,小的、常更新的可以使用软链接项目的方式。

后续

更多博客,查看 https://github.com/senntyou/blogs

作者:深予之 (@senntyou)

版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证

查看原文

赞 12 收藏 10 评论 0

senntyou 发布了文章 · 2019-06-06

js 函数参数推荐书写方式 ({ param1, param2, ... })

js 函数参数推荐书写方式 ({ param1, param2, ... })

编程语言函数(包括对象的方法)参数的取值方式大致可以分为两种:按序取值与按名取值。

一般编程语言都是按序取值,比如 C、Java、JavaScript 等,少数语言支持按名取值,比如 Groovy。

1. 按序取值

按照顺序,挨个取值,每个参数的顺序是固定的。

const func = (param1, param2, ...) => { ... }

func(1, 2, ...)

2. 按名取值

按照名称取值,可以任意安排各个参数的顺序。

以下语法并不存在,只是作为讲解生造的
const func = (param1: value1, param2: value2, ...) => { ... }

func(param1: 1, param2: 2); // ok 
func(param2: 2, param1: 1); // ok again 

3. js 的按名取值

JavaScript 语言本身并不支持按名取值,但结合 ES6 的解构赋值,可以模拟函数参数的按名取值。

const func = ({ param1, param2, ... }) => { ... }

func({ param1: 1, param2: 2, ... });

但这种方式如果不传参数调用 func() 就会报错,需要 func({}) 这样调用才表示什么参数都不传。

为了兼容这种方式,可以这样做:

const func = ({ param1, param2, ... } = {}) => { ... }

func();   // ok
func({}); // ok again

4. 为什么推荐使用按名取值的方式

按名取值最大的好处是可以随意安排参数的顺序,有利于扩展,特别是对 API 接口来说。

比如:

export const dialog = (title, content, confirmCallback, cancelCallback) => { ... }

比如上面的函数中,大部分情况下我只用 content, confirmCallback,那么我就需要这样做:

dialog(null, 'content', () => { ... });

如果我需要扩展一个参数 icon, 那么为了兼容以前的版本,我只能加在最后面:

export const dialog = (title, content, confirmCallback, cancelCallback, icon) => { ... }

现在,大部分情况下我只用 content, confirmCallback, icon,那么我就需要这样做:

dialog(null, 'content', () => { ... }, null, 'icon');

如此,便很麻烦,不利于扩展。

如果使用按名取值的方式,便迎刃而解:

export const dialog = ({title, content, confirmCallback, cancelCallback} = {}) => { ... }

// 扩展 icon
export const dialog = ({title, content, icon, confirmCallback, cancelCallback} = {}) => { ... }
dialog({content: 'content', confirmCallback: () => { ... }});

dialog({content: 'content', icon: 'icon', confirmCallback: () => { ... }});

5. 大家可能的反驳

有人可能会说,可以这样做:

export const dialog = (title, content, confirmCallback, cancelCallback) => { 
  if (typeof content === 'function') {
    cancelCallback = confirmCallback;
    confirmCallback = content;
    content = title;
  }
  
  ...
}

对于这种方式,我只想说:兄弟,简洁一点不好吗?

后续

更多博客,查看 https://github.com/senntyou/blogs

作者:深予之 (@senntyou)

版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证

查看原文

赞 12 收藏 6 评论 2

senntyou 发布了文章 · 2019-05-30

细说 Vue 组件的服务器端渲染

细说 Vue 组件的服务器端渲染

声明:需要读者对 NodeJs、Vue 服务器端渲染有一定的了解

现在,前后端分离与客户端渲染已经成为前端开发的主流模式,绝大部分的前端应用都适合用这种方式来开发,又特别是 React、Vue 等组件技术的发展,更是使这种方式深入人心。

但有一些应用,客户端渲染就会遇到一些问题了:

  1. 需要做 SEO(搜索引擎优化),但客户端渲染的 html 中几乎没有可用的信息
  2. 需要首屏快速加载,但客户端渲染一般是长时间的加载动画或者白屏

如果能把客户端渲染的组件化技术(React、Vue 等)与传统的后端渲染的方式有效的结合起来,两者兼具,那就太完美了。

所以,这次就来聊聊 Vue 组件的服务器端渲染。

根据社区现有的一些方案,结合自己的实践,针对团队技术力量的不同,说说不同应用场景选择方案时的优先级。

1. NodeJs 渲染中间层

一般前后端的工作流是 后端 -> 前端

传统的后端渲染模式是后端负责包括 url、接口、模板渲染等,前端与后端耦合在一起,当然这种方式正在慢慢的退出历史舞台。

主流的客户端渲染则是后端只提供接口(如有需要,可以提供必要的 url),前端与后端只通过接口交流数据,路由与渲染都在前端完成。

而 NodeJs 渲染中间层的工作流则是 后端 -> NodeJs -> 前端(NodeJs 渲染中间层由前端开发人员掌握)。

图片描述

这种模式下,后端只提供接口,传统的服务器端路由(url)、模板渲染则都有 NodeJs 层接管。这样,前端开发人员可以自由的决定哪些组件需要在服务器端渲染,哪些组件可以放在客户端渲染,前后端完全解耦,但又保留了服务器端渲染的功能。

这种方案最成熟的是 nuxt.js

如果有需要,大家可以自己去 nuxt.js 官方文档 看看具体的使用方法和详细的功能。

应该说,这种方式是目前最完美的一种方案,但也有一些隐患:

  1. 增加了一个 NodeJs 中间层,应用性能会有所降低
  2. 增加了架构的复杂度、不稳定性,也降低了应用的安全性
  3. 对于高并发应用,NodeJs 层会很容易形成瓶颈
  4. 对开发人员要求高了很多

所以,这种方式适合对并发量、安全性、稳定性等要求不高,但又需要做 SEO 或首屏快速加载的页面。

当然,如果你能够自己改造相关的工具,就另当别论了。

2. 保留后端模板渲染

当不能使用 NodeJs 中间层时,而又要达到 SEO 与首屏快速响应的目的时,在传统的后端模板渲染的基础上,就需要对前端的页面加以适当的改造。

2.1 首屏快速响应

首屏快速响应就意味着首屏渲染所需的数据是跟 HTML 文件一起到达浏览器的,这些数据当前是由后端模板引擎嵌入到 HTML 页面中的。

以 Java 的 freemarker 模板引擎为例:

html 中以 script 的方式获取模板的数据,这样就算是在本地调试、开发,也不会报错)。

<script>
  window.globalData = {
    stringValue: '${stringValueTplName}',
    intValue: parseInt('${intValueTplName}', 10),
  };
</script>

如果是复杂的 Json 数据或者其他复杂的模板数据(比如列表数据),则可以像下面这样接收:

<script type="text/tpl" id="tpl-script-json">
  window.tmpData = {
    jsonValue: ${jsonValueTplName},
  };
</script>

<script>
  try {
    eval(document.getElementById('tpl-script-json').innerText);
  } catch (e) {
    window.tmpData = { jsonValue: {} };
  }
  
  window.globalData = {
    jsonValue: window.tmpData.jsonValue,
  };
</script>

这样,你就可以在组件里使用 window.globalData 的数据了,而不用另外用接口获取数据,达到加快首屏渲染的目的,而且本地开发、调试也不会报错。

如果你使用了本地数据 Mock 功能,也可以很容易的与这种方式结合在一起,只要稍加改造:

  1. 在代码中定义本地和服务器两个环境,本地环境使用 Mock 数据,服务器环境使用 window.globalData
  2. 可以使用 see-ajax, see-fetch 来简化这种方式的开发

此外,还有一些措施来进一步加快首屏渲染:

  1. 尽量减少首屏加载的脚本文件大小,其他脚本可以按需加载
  2. 如果需要,可以将 CSS、JS 内容注入到 HTML 中,这样就只会发起一个请求,也可以加快加载速度

2.2 SEO 优化

在上面加载首屏渲染的基础上,对于 SEO 优化也可以做相应的改造。

其实,在客户端渲染已慢慢成为主流开发模式的同时,搜索引擎也在跟进这种变化。

截至目前,Google 和 Bing 可以很好对同步 JavaScript 应用程序进行索引,也就是说,即使是客户端渲染,但只要是同步数据渲染(非 Ajax 获取数据,比如模板数据),搜索引擎也能抓取到相应的 HTML 片段。

(国内的百度搜索与360搜索等暂时还没有跟进动态)

但为了兼容所有的搜索引擎,可以像下面改造:

  1. 先由后端模板引擎渲染一些 HTML 片段,仅给搜索引擎抓取,不作为给用户展示的页面
  2. 然后再由客户端渲染同步或异步的数据给用户展示真正的页面
<div>
  <!-- 这里放置由后端模板引擎渲染的专给搜索引擎抓取的片段,用户不可见 -->
</div>

<script>
  // 接收同步数据
  window.globalData = {
    stringValue: '${stringValueTplName}',
    intValue: parseInt('${intValueTplName}', 10),
  };
</script>

3. 导出静态 html

如果页面没有动态数据,那就好办了,直接把组件导出为静态 html,然后由客户端激活。

具体过程可以参考 官方文档

这种方案比较好的是 nuxt.jsgenerate 静态 HTML 文件。

目录结构:

- pages/                # 页面结构目录
  - index.vue 
  - second.vue
  - ... 
- nuxt.config.js        # 配置文件
- package.json

- dist                  # 导出静态 HTML 文件的默认目录 

导出静态 HTML 文件

npx nuxt generate

如果一个项目里有多个 pages,可以这样构建:

目录结构:

- nuxt.config.js        # 配置文件
- package.json

- src/
  - home/               # home 页面 
    - pages/            # 页面结构目录
      - index.vue 
      - second.vue
      - ...
      
    - dist              # 导出静态 HTML 文件的默认目录
  - about/              # about 页面 
    - pages/            # 页面结构目录
      - index.vue 
      - second.vue
      - ...
      
    - dist              # 导出静态 HTML 文件的默认目录   

导出静态 HTML 文件

npx nuxt generate src/home -c ../../nuxt.config.js    # home 页面
npx nuxt generate src/about -c ../../nuxt.config.js   # about 页面

除了上面提到的这些方式外,当然还有其他的方式,比如:

  1. 使用 Chrome Headless 模式获取组件的静态 HTML,参考 react-snap, puppeteer
  2. 官方 vue-server-renderer 导出静态 HTML

4. 总结

因为模式的改变,服务器端渲染与传统的后端模板渲染工作方式有很大的不同,所以在开发时需要与后端开发人员做好沟通,避免认知上的不同导致协作不协调。

后续

更多博客,查看 https://github.com/senntyou/blogs

作者:深予之 (@senntyou)

版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证

查看原文

赞 15 收藏 13 评论 0

senntyou 发布了文章 · 2019-03-29

如何查看一个 js, ts 文件模块的依赖树

如何查看一个 js, ts 文件模块的依赖树

最近接手一个别人的页面,代码很是凌乱与庞杂,当我在增加功能时,发现我添加的模块与原有的模块有冲突,但不知道那个原有的模块在什么位置,就得到处找。

但发现这种方式太笨拙了,网上也没有找到相应的工具,索性我就自己写了一个工具。

项目地址:sdep

安装

npm install sdep -g

使用

sdep [options] <file>

常用命令行参数

  • -q, --query <query>: 查找某个模块的依赖链(如:-q react
  • -r, --regular: 把 query 当作正则来匹配(如:-q 'react|react-dom' -r
  • -i, --ignore: 不显示 node_modules 里的文件

使用的第三方库

例子

查看一个文件的依赖结构

sdep example/index.js
example/index.js
├ example/css/css.css
├ example/css/scss.scss
├ example/css/less.less
├ example/jsx.jsx
| ├ node_modules/react/index.js
| | ├ node_modules/react/cjs/react.production.min.js
| | | └ node_modules/object-assign/index.js
| | └ node_modules/react/cjs/react.development.js
| |   ├ node_modules/object-assign/index.js
| |   └ node_modules/prop-types/checkPropTypes.js
| |     └ node_modules/prop-types/lib/ReactPropTypesSecret.js
| ├ node_modules/react-dom/index.js
| | ├ node_modules/react-dom/cjs/react-dom.production.min.js
| | | ├ node_modules/react/index.js
| | | | ├ node_modules/react/cjs/react.production.min.js
| | | | | └ node_modules/object-assign/index.js
| | | | └ node_modules/react/cjs/react.development.js
| | | |   ├ node_modules/object-assign/index.js
| | | |   └ node_modules/prop-types/checkPropTypes.js
| | | |     └ node_modules/prop-types/lib/ReactPropTypesSecret.js
| | | ├ node_modules/object-assign/index.js
| | | └ node_modules/scheduler/index.js
| | |   ├ node_modules/scheduler/cjs/scheduler.production.min.js
| | |   └ node_modules/scheduler/cjs/scheduler.development.js
| | └ node_modules/react-dom/cjs/react-dom.development.js
| |   ├ node_modules/react/index.js
| |   | ├ node_modules/react/cjs/react.production.min.js
| |   | | └ node_modules/object-assign/index.js
| |   | └ node_modules/react/cjs/react.development.js
| |   |   ├ node_modules/object-assign/index.js
| |   |   └ node_modules/prop-types/checkPropTypes.js
| |   |     └ node_modules/prop-types/lib/ReactPropTypesSecret.js
| |   ├ node_modules/object-assign/index.js
| |   ├ node_modules/prop-types/checkPropTypes.js
| |   | └ node_modules/prop-types/lib/ReactPropTypesSecret.js
| |   ├ node_modules/scheduler/index.js
| |   | ├ node_modules/scheduler/cjs/scheduler.production.min.js
| |   | └ node_modules/scheduler/cjs/scheduler.development.js
| |   └ node_modules/scheduler/tracing.js
| |     ├ node_modules/scheduler/cjs/scheduler-tracing.production.min.js
| |     └ node_modules/scheduler/cjs/scheduler-tracing.development.js
| └ example/wel.jsx
|   └ node_modules/react/index.js
|     ├ node_modules/react/cjs/react.production.min.js
|     | └ node_modules/object-assign/index.js
|     └ node_modules/react/cjs/react.development.js
|       ├ node_modules/object-assign/index.js
|       └ node_modules/prop-types/checkPropTypes.js
|         └ node_modules/prop-types/lib/ReactPropTypesSecret.js
└ example/async/index.js
  └ example/async/index.css

查看一个文件包含 react 的依赖链

sdep example/index.js -q react
example/index.js
└ example/jsx.jsx
  └ node_modules/react/index.js
example/index.js
└ example/jsx.jsx
  └ node_modules/react-dom/index.js
example/index.js
└ example/jsx.jsx
  └ example/wel.jsx
    └ node_modules/react/index.js

查看一个文件包含 lessscss 的依赖链

sdep example/index.js -q 'less|scss' -r
example/index.js
└ example/css/scss.scss
example/index.js
└ example/css/less.less

后续

更多博客,查看 https://github.com/senntyou/blogs

作者:深予之 (@senntyou)

版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证

查看原文

赞 16 收藏 13 评论 0

senntyou 评论了文章 · 2019-03-22

从 0 到 1 再到 100, 搭建、编写、构建一个前端项目

从 0 到 1 再到 100, 搭建、编写、构建一个前端项目

1. 选择现成的项目模板还是自己搭建项目骨架

搭建一个前端项目的方式有两种:选择现成的项目模板、自己搭建项目骨架。

选择一个现成项目模板是搭建一个项目最快的方式,模板已经把基本的骨架都搭建好了,你只需要向里面填充具体的业务代码,就可以通过内置的工具与命令构建代码、部署到服务器等。

一般来说,一个现成的项目模板会预定义一定的目录结构、书写方式,在编写项目代码时需要遵循相应的规范;也会内置必要的工具,比如 .editorconfigeslintstylelintprettierhuskylint-staged 等;也会内置必要的命令(package.json | scripts),比如 本地开发:npm run dev本地预览:npm run start构建:npm run build部署:npm run deploy 等。

社区比较好的项目模板:

这些模板的使用又分为两种:使用 git 直接克隆到本地、使用命令行创建。

(使用现有模板构建的项目,可以跳过第 2 ~ 7 步)

1.1 使用 git 直接克隆到本地

这是一种真正意义上的模板,可以直接到模板项目的 github 主页,就能看到整个骨架,比如 react-boilerplateant-design-provue-element-adminreact-starter-kit

react-boilerplate 为例:

克隆到本地:

git clone --depth=1 https://github.com/react-boilerplate/react-boilerplate.git <你的项目名字>

切换到目录下:

cd <你的项目名字>

一般来说,接下来运行 npm run install 安装项目的依赖后,就可以运行;有些模板可能有内置的初始化命令,比如 react-boilerplate

npm run setup

启动应用:

npm start

这时,就可以在浏览器中预览应用了。

1.2 使用命令行创建

这种方式需要安装相应的命令,然后由命令来创建项目。

create-react-app 为例:

安装命令:

npm install -g create-react-app

创建项目:

create-react-app my-app

运行应用:

cd my-app
npm start

1.3 自己搭建项目骨架

如果你需要定制化,可以选择自己搭建项目的骨架,但这需要开发者对构建工具如 webpacknpmnode 及其生态等有相当的了解与应用,才能完美的把控整个项目。

下面将会一步一步的说明如何搭建一个定制化的项目骨架。

2. 选择合适的规范来写代码

js 模块化的发展大致有这样一个过程 iife => commonjs/amd => es6,而在这几个规范中:

  • iife: js 原生支持,但一般不会直接使用这种规范写代码
  • amd: requirejs 定义的加载规范,但随着构建工具的出现,便一般不会用这种规范写代码
  • commonjs: node 的模块加载规范,一般会用这种规范写 node 程序
  • es6: ECMAScript2015 定义的模块加载规范,需要转码后浏览器才能运行

这里推荐使用 es6 的模块化规范来写代码,然后用工具转换成 es5 的代码,并且 es6 的代码可以使用 Tree shaking 功能。

参考:

3. 选择合适的构建工具

对于前端项目来说,构建工具一般都选用 webpackwebpack 提供了强大的功能和配置化运行。如果你不喜欢复杂的配置,可以尝试 parcel

参考:

4. 确定是单页面应用(SPA)还是多页面应用

因为单页面应用与多页面应用在构建的方式上有很大的不同,所以需要从项目一开始就确定,使用哪种模式来构建项目。

4.1 多页面应用

传统多页面是由后端控制一个 url 对应一个 html 文件,页面之间的跳转需要根据后端给出的 url 跳转到新的 html 上。比如:

http://www.example.com/page1 -> path/to/page1.html
http://www.example.com/page2 -> path/to/page2.html
http://www.example.com/page3 -> path/to/page3.html

这种方式的应用,项目里会有多个入口文件,搭建项目的时候就需要对这种多入口模式进行封装。另外,也可以选择一些封装的多入口构建工具,如 lila

4.2 单页面应用

单页面应用(single page application),就是只有一个页面的应用,页面的刷新和内部子页面的跳转完全由 js 来控制。

一般单页面应用都有以下几个特点:

  • 本地路由,由 js 定义路由、根据路由渲染页面、控制页面的跳转
  • 所有文件只会加载一次,最大限度重用文件,并且极大提升加载速度
  • 按需加载,只有真正使用到页面的时候,才加载相应的文件

这种方式的应用,项目里只有一个入口文件,便无需封装。

参考:

5. 选择合适的前端框架与 UI 库

一般在搭建项目的时候就需要定下前端框架与 UI 库,因为如果后期想更换前端框架和 UI 库,代价是很大的。

比较现代化的前端框架:

一些不错的组合:

参考:

6. 定好目录结构

一个好的目录结构对一个好的项目而言是非常必要的。

一个好的目录结构应当具有以下的一些特点:

  1. 解耦:代码尽量去耦合,这样代码逻辑清晰,也容易扩展
  2. 分块:按照功能对代码进行分块、分组,并能快捷的添加分块、分组
  3. 编辑器友好:需要更新功能时,可以很快的定位到相关文件,并且这些文件应该是很靠近的,而不至于到处找文件

比较推荐的目录结构:

多页面应用

|-- src/ 源代码目录

    |-- page1/ page1 页面的工作空间(与这个页面相关的文件都放在这个目录下)
        |-- index.html html 入口文件
        |-- index.js js 入口文件
        |-- index.(css|less|scss) 样式入口文件
        |-- html/ html 片段目录
        |-- (css|less|scss)/ 样式文件目录
        |-- mock/ 本地 json 数据模拟
        |-- images/ 图片文件目录
        |-- components/ 组件目录(如果基于 react, vue 等组件化框架)
        |-- ...
        
    |-- sub-dir/ 子目录
        |-- page2/ page2 页面的工作空间(内部结构参考 page1)
            |-- ...
        
    |-- ...
    
|-- html/ 公共 html 片段
|-- less/ 公共 less 目录
|-- components/ 公共组件目录
|-- images/ 公共图片目录
|-- mock/ 公共 api-mock 文件目录
|-- ...

单页面应用

|-- src/ 源代码目录
    |-- page1/ page1 页面的工作空间
        |-- index.js 入口文件
        |-- services/ service 目录
        |-- models/ model 目录
        |-- mock/ 本地 json 数据模拟
        |-- images/ 图片文件目录
        |-- components/ 组件目录(如果基于 react, vue 等组件化框架)
        |-- ...
        
    |-- module1/ 子目录
        |-- page2/ page2 页面的工作空间(内部结构参考 page1)
        
    |-- ...
    
|-- images/ 公共图片目录
|-- mock/ 公共 api-mock 文件目录
|-- components/ 公共组件目录   
|-- ... 

参考:

7. 搭建一个好的脚手架

搭建一个好的脚手架,能够更好的编写代码、构建项目等。

可以查看 搭建自己的前端脚手架 了解一些基本的脚手架文件与工具。

比如:

|-- /                              项目根目录
    |-- src/                       源代码目录
    |-- package.json               npm 项目文件
    |-- README.md                  项目说明文件
    |-- CHANGELOG.md               版本更新记录
    |-- .gitignore                 git 忽略配置文件
    |-- .editorconfig              编辑器配置文件
    |-- .npmrc                     npm 配置文件
    |-- .npmignore                 npm 忽略配置文件
    |-- .eslintrc                  eslint 配置文件
    |-- .eslintignore              eslint 忽略配置文件
    |-- .stylelintrc               stylelint 配置文件
    |-- .stylelintignore           stylelint 忽略配置文件
    |-- .prettierrc                prettier 配置文件
    |-- .prettierignore            prettier 忽略配置文件
    
    |-- .babelrc                   babel 配置文件
    |-- webpack.config.js          webpack 配置文件
    |-- rollup.config.js           rollup 配置文件
    |-- gulpfile.js                gulp 配置文件
    
    |-- test/                      测试目录
    |-- docs/                      文档目录
    |-- jest.config.js             jest 配置文件
    |-- .gitattributes             git 属性配置
  • .editorconfig: 用这个文件来统一不同编辑器的一些配置,比如 tab 转 2 个空格、自动插入空尾行、去掉行尾的空格等,http://editorconfig.org
  • eslintstylelintprettier: 规范化代码风格、优化代码格式等
  • huskylint-staged: 在 git 提交之前对代码进行审查,否则不予提交
  • .gitlab-ci.yml: gitlab ci 持续集成服务

参考:

=================================================

到这里为止,一个基本的项目骨架就算搭建好了。

8. 使用版本控制系统管理源代码(git)

项目搭建好后,需要一个版本控制系统来管理源代码。

比较常用的版本管理工具有 gitsvn,现在一般都用 git

一般开源的项目可以托管到 http://github.com,私人的项目可以托管到 https://gitee.comhttps://coding.net/,而企业的项目则需要自建版本控制系统了。

自建版本控制系统主要有 gitlabgogsgiteagitlab 是由商业驱动的,比较稳定,社区版是免费的,一般建议选用这个;gogs, gitea 是开源的项目,还不太稳定,期待进一步的更新。

所以,git + gitlab 是不错的配合。

9. 编写代码

编写代码时,js 选用 es6 的模块化规范来写(如果喜欢用 TypeScript,需要加上 ts-loader),样式可以用 lessscsscss 来写。

js 模块文件时,注释可以使用 jsdoc 的规范来写,如果配置相应的工具,可以将这些注释导出接口文档。

因为脚手架里有 huskylint-staged 的配合,所以每次提交的代码都会进行代码审查与格式优化,如果不符合规范,则需要把不规范的代码进行修改,然后才能提交到代码仓库中。

比如 console.log(haha.hehe); 这段代码就会遇到错误,不予提交:

图片描述

这个功能定义在 package.json 中:

{
  "devDependencies": {             工具依赖
    "babel-eslint": "^8.2.6",
    "eslint": "^4.19.1",
    "husky": "^0.14.3",
    "lint-staged": "^7.2.0",
    "prettier": "^1.14.0",
    "stylelint": "^9.3.0",
    "eslint-config-airbnb": "^17.0.0",
    "eslint-config-prettier": "^2.9.0",
    "eslint-plugin-babel": "^5.1.0",
    "eslint-plugin-import": "^2.13.0",
    "eslint-plugin-jsx-a11y": "^6.1.0",
    "eslint-plugin-prettier": "^2.6.2",
    "eslint-plugin-react": "^7.10.0",
    "stylelint-config-prettier": "^3.3.0",
    "stylelint-config-standard": "^18.2.0"
  },
  "scripts": {                     可以添加更多命令
    "precommit": "npm run lint-staged",
    "prettier": "prettier --write \"./**/*.{js,jsx,css,less,sass,scss,md,json}\"",
    "eslint": "eslint .",
    "eslint:fix": "eslint . --fix",
    "stylelint": "stylelint \"./**/*.{css,less,sass,scss}\"",
    "stylelint:fix": "stylelint \"./**/*.{css,less,sass,scss}\" --fix",
    "lint-staged": "lint-staged"
  },
  "lint-staged": {                 对提交的代码进行检查与矫正
    "**/*.{js,jsx}": [
      "eslint --fix",
      "prettier --write",
      "git add"
    ],
    "**/*.{css,less,sass,scss}": [
      "stylelint --fix",
      "prettier --write",
      "git add"
    ],
    "**/*.{md,json}": [
      "prettier --write",
      "git add"
    ]
  }
}
  • 如果你想禁用这个功能,可以把 scripts"precommit" 改成 "//precommit"
  • 如果你想自定 eslint 检查代码的规范,可以修改 .eslintrc, .eslintrc.js 等配置文件
  • 如果你想自定 stylelint 检查代码的规范,可以修改 .stylelintrc, .stylelintrc.js 等配置文件
  • 如果你想忽略某些文件不进行代码检查,可以修改 .eslintignore, .stylelintignore 配置文件

参考:

10. 组件化

当项目拥有了一定量的代码之后,就会发现,有些代码是很多页面共用的,于是把这些代码提取出来,封装成一个组件,供各个地方使用。

当拥有多个项目的时候,有些组件需要跨项目使用,一种方式是复制代码到其他项目中,但这种方式会导致组件代码很难维护,所以,一般是用另一种方式:组件化。

组件化就是将组件独立成一个项目,然后在其他项目中安装这个组件,才能使用。

一般组件化会配合私有 npm 仓库一起用。

|-- project1/ 项目1
    |-- package.json
    
|-- project2/ 项目2
    |-- package.json    

|-- component1/ 组件1
    |-- package.json

|-- component2/ 组件2
    |-- package.json

project1 中安装 component1, component2 组件:

# package.json
{
  "dependencies": {
    "component1": "^0.0.1",
    "component2": "^0.0.1"
  }
}
import compoennt1 from 'compoennt1';
import compoennt2 from 'compoennt2';

如果想要了解怎样写好一个组件(npm package),可以参考 从 1 到完美,写一个 js 库、node 库、前端组件库

参考:

11. 测试

测试的目的在于能以最少的人力和时间发现潜在的各种错误和缺陷,这在项目更新、重构等的过程中尤其重要,因为每当更改一些代码后,你并不知道这些代码有没有问题、会不会影响其他的模块。如果有了测试,运行一遍测试用例,就知道更改的代码有没有问题、会不会产生影响。

一般前端测试分以下几种:

  1. 单元测试:模块单元、函数单元、组件单元等的单元块的测试
  2. 集成测试:接口依赖(ajax)、I/O 依赖、环境依赖(localStorage、IndexedDB)等的上下文的集成测试
  3. 样式测试:对样式的测试
  4. E2E 测试:端到端测试,也就是在实际生产环境测试整个应用

一般会用到下面的一些工具:

另外,可以参考 聊聊前端开发的测试

12. 构建

一般单页面应用的构建会有 npm run build 的命令来构建项目,然后会输出一个 html 文件,一些 js/css/images ... 文件,然后把这些文件部署到服务器就可以了。

多页面应用的构建要复杂一些,因为是多入口的,所以一般会封装构建工具,然后通过参数传入多个入口:

npm run build -- page1 page2 dir1/* dir2/all --env test/prod
  • page1, page2 确定构建哪些页面;dir1/*, dir2/all 某个目录下所有的页面;all, * 整个项目所有的页面
  • 有时候可能还会针对不同的服务器环境(比如测试机、正式机)做出不同的构建,可以在后面加参数
  • -- 用来分割 npm 本身的参数与脚本参数,参考 npm - run-script 了解详情

多页面应用会导出多个 html 文件,需要注意这些导出的 html 不要相冲突了。

当然,也可以用一些已经封装好的工具,如 lila

13. 部署

在构建好项目之后,就可以部署到服务器了。

传统的方式,可以用 ftp, sftp 等工具,手动传到服务器,但这种方式比较笨拙,不够自动化。

自动化的,可以用一些工具部署到服务器,如 gulpgulp-ssh,当然也可以用一些封装的工具,如 md-synclila

md-sync 为例:

npm install md-sync --save-dev

md-sync.config.js 配置文件:

module.exports = [
  {
    src: './build/**/*',
    remotePath: 'remotePath',
    server: {
      ignoreErrors: true,
      sshConfig: {
        host: 'host',
        username: 'username',
        password: 'password'
      }
    },
  },
  {
    src: './build/**/*.html',
    remotePath: 'remotePath2',
    server: {
      ignoreErrors: true,
      sshConfig: {
        host: 'host',
        username: 'username',
        password: 'password'
      }
    },
  },
];

package.jsonscripts 配置好命令:

"scripts": {
  "deploy": "md-sync"
}
npm run deploy

另外,一般大型项目会使用持续集成 + shell 命令(如 rsync)部署。

14. 持续集成测试、构建、部署

一般大型工程的的构建与测试都会花很长的时间,在本地做这些事情的话就不太实际,这就需要做持续集成测试、构建、部署了。

持续集成工具用的比较多的:

jenkins 是通用型的工具,可以与 githubbitbucketgitlab 等代码托管服务配合使用,优点是功能强大、插件多、社区活跃,但缺点是配置复杂、使用难度较高。

gitlab cigitlab 内部自带的持续集成功能,优点是使用简单、配置简单,但缺点是不及 jenkins 功能强大、绑定 gitlab 才能使用。

gitlab 为例(任务定义在 .gitlab-ci.yml 中):

stages:
  - install
  - test
  - build
  - deploy

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

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

# 编译
build:
  stage: build
  only:
    - dev
    - master
  script:
    - npm run clean
    - npm run build

# 部署服务器
deploy:
  stage: deploy
  only:
    - dev
    - master
  script:
    - npm run deploy

以上配置表示只要在 devmaster 分支有代码推送,就会进行持续集成,依次运行:

  • npm install
  • npm run test
  • npm run clean
  • npm run build
  • npm run deploy

最终完成部署。如果中间某个命令失败了,将停止接下的命令的运行,并将错误报告给你。

这些操作都在远程机器上完成。

=================================================

到这里为止,基本上完成了一个项目的搭建、编写、构建。

15. 清理服务器上过期文件

现在前端的项目基本上都会用 webpack 打包代码,并且文件名(html 文件除外)都是 hash 化的,如果需要清除过期的文件而又不想把服务器上文件全部删掉然后重新构建、部署,可以使用 sclean 来清除过期文件。

16. 收集前端错误反馈

当用户在用线上的程序时,怎么知道有没有出 bug;如果出 bug 了,报的是什么错;如果是 js 报错,怎么知道是那一行运行出了错?

所以,在程序运行时捕捉 js 脚本错误,并上报到服务器,是非常有必要的。

这里就要用到 window.onerror 了:

window.onerror = (errorMessage, scriptURI, lineNumber, columnNumber, errorObj) => {
  const data = {
    title: document.getElementsByTagName('title')[0].innerText,
    errorMessage,
    scriptURI,
    lineNumber,
    columnNumber,
    detailMessage: (errorObj && errorObj.message) || '',
    stack: (errorObj && errorObj.stack) || '',
    userAgent: window.navigator.userAgent,
    locationHref: window.location.href,
    cookie: window.document.cookie,
  };

  post('url', data); // 上报到服务器
};

线上的 js 脚本都是压缩过的,需要用 sourcemap 文件与 source-map 查看原始的报错堆栈信息,可以参考 细说 js 压缩、sourcemap、通过 sourcemap 查找原始报错信息 了解详细信息。

参考:

后续

更多博客,查看 https://github.com/senntyou/blogs

作者:深予之 (@senntyou)

版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证

查看原文

认证与成就

  • 获得 4607 次点赞
  • 获得 20 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 20 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

  • lila

    多入口、基于任务的构建工具封装器,webpack、gulp、rollup ...

  • see-ajax

    对 ajax 的封装

  • see-fetch

    对 window.fetch 的封装

  • diary

    用 react-native + expo 开发的日记 APP

  • image-viewer

    用 electron 开发的图片浏览桌面软件

  • sclean

    清除服务器端过期的 hash 文件

注册于 2018-04-25
个人主页被 9.1k 人浏览