夜聆风

夜聆风 查看完整档案

北京编辑大连东软信息学院  |  软件技术 编辑chanct  |  web码农 编辑 github.com/yelingfeng 编辑
编辑

前端码农

个人动态

夜聆风 收藏了文章 · 2019-05-05

用vscode开发vue应用

现在用VSCode开发Vue.js应用几乎已经是前端的标配了,但很多时候我们看到的代码混乱不堪,作为一个前端工程师,单引号双引号乱用,一段有分号一段没有分号,有的地方有逗号有的地方没有逗号,空格回车都对不齐,还说自己做事认真,这不是开玩笑的事情。

clipboard.png

我们今天从头开始,完整地讲述一下一个重度代码洁癖患者该如何用vscode开发vue,以及如何把一个已经可以宣判死刑的全身各种格式错误几万条的项目整容成标准美女。

从安装开始

为了准确起见,我们把vscode里所有插件全部禁用,把用户设置清空,以让它尽可能恢复成原始的样子:

clipboard.png

作为代码洁癖患者,对于系统的版本要求一定也是最苛刻的,不管什么时候,都让我们把所有的系统能升级的都升级到最高版本:

npm install -g @vue/cli

然后,我们开始创建项目:

vue create hello-world

在这里,一定要选择第一项:babel + eslint,这两个是必不可少的。我见到有些人嫌eslint麻烦,居然在项目建立好之后手工把eslint关掉的,简直无语。

clipboard.png

安装完毕:

clipboard.png

我们先不急着执行,执行代码是最容易的事情,我们先打开代码看一下:

clipboard.png

好吧,至少我们需要先安装vetur插件。这几乎已经确定是开发vue项目的标配了,即使我不说,vscode也会强烈建议你安装它。

clipboard.png

装上vetur以后多少有点人样了。接下来我们来试一试能不能自动格式化,这部分才是洁癖患者的最爱。胡乱加几个空格,然后保存试试看:

clipboard.png

不能格式化,连个提示都没有!

用lint格式化

就算vscode里的vetur不能帮我们自动格式化,好在package.json命令里还有一个lint命令,我们看看lint命令能不能帮我们自动格式化:

clipboard.png

lint居然说没有错误!明明就是多了很多空格的错误好吧,为什么?

这是因为缺省的vue-cli没有为我们安装@vue/prettier插件,我们先来手工安装一下:

yarn add -D @vue/eslint-config-prettier

然后在package.json或者.eslintrc.js或者其它什么你设置eslint的地方,给它加上:

    "extends": [
      "plugin:vue/essential",
      "eslint:recommended",
      "@vue/prettier"
    ],

特别是最后这一个@vue/prettier,非常重要。然后再执行yarn lint。多余的空格被自动干掉了,但是我们发现有一些地方同时也被篡改了:

clipboard.png

所有的单引号被变成双引号了,原本行尾没有的分号被加上了分号。这是为什么呢?因为我们虽然引入了prettier,但是还没有对prettier做设置,我们在项目的根目录下创建一个.prettierrc.js文件,然后在其中加入:

module.exports = {
  semi: false,
  singleQuote: true
}

再次执行yarn lint,现在我们看到lint已经能够起作用了。它不但能把我们多余插入的空格删掉,并且能按照规则把双引号变成单引号,把行尾多余的分号删掉。当然,关于行尾加不加分号这是一个哲学命题,你可以根据你个人的喜好自行决定。在这里,我们权且按照vue-cli的标配执行。

图片描述

到这一步很关键,假设你拿到一个烂的不再烂的vue项目,里面有几千个.vue文件,几万个各种格式错误,也都能通过yarn lint这一行命令把它们全部修正过来!

在vscode里格式化

事情还没有完,我们注意到虽然yarn lint命令能在编写完代码之后帮我们格式化,但是既然我们是用vscode进行开发,我们当然希望能在vscode里直接看到对于错误的标注。

这时候我们需要在vscode里再安装一个插件eslint

你以为安装上eslint插件就行了吗?不行的。因为eslint并不知道我们的.vue文件里面包含了js语法,所以还需要打开我们的vscode设置文件。

注意:这里一定要设置到项目的设置里,而不要只是设置到你自己个人的设置里,否则你团队的小伙伴随便一改又乱掉了。正确的方法是在你项目的文件夹下有一个.vscode文件夹,而vue最讨厌的地方是它居然会把这个文件夹放到.gitignore里,这个错误你必须要纠正过来,也就是说从.gitignore文件里把.vscode删掉。切切。

在你项目的settings.json里文件里添加以下代码:

{
  "eslint.autoFixOnSave": true,
  "eslint.validate": [
    "javascript",
    "javascriptreact",
    {
      "language": "vue",
      "autoFix": true
    }
  ],
}

这时候所有错误都被标注出来了,注意看左侧,一定要让这个settings.json文件是绿色的,而不能是灰色的,如果是灰色的,请检查你的.gitignore文件:

clipboard.png

因为我们在settings.json文件里设置了autoFixOnSave,所以不管多么乱的格式,只要一按Ctrl+S保存,自动就帮我们把代码格式整理好了,是不是很方便呢?

和Prettier的冲突

有些时候我们的vscode里插件装的比较多,譬如还安装了prettier插件,因为我们不只开发vue项目,可能还有其它类型的js项目特别是传统js项目,需要用到prettier进行美化,而prettier的一些功能是会和eslint相冲突的,比如说我们在全局设置了prettierformatOnSave,这个功能就会和eslintautoFixOnSave打架,为了避免这个矛盾,我们通常还会在本项目的settings.json文件里再多加几个选项,类似于这样:

  "editor.tabSize": 2,
  "editor.formatOnSave": false,
  "prettier.semi": false,
  "prettier.singleQuote": true

有了这些设置,基本上prettier就不会和eslint打架了。

小结

以上就是用vscode开发vue程序的标配,并不像网上有些文章说的那么简单,不是只需要配一个eslint就能解决的事情,这里还用到了vetureslintprettier,把几个工具综合用好,才能真正达到我们的错误随时可见,保存自动修改,更正既往错误的目的。希望每个前端工程师写出的代码都如出一人之手,漂亮简洁干净。

我们的目标始终如一:0错误0警告


关于如何在vscode中进行规范化的Vue应用开发,我做了一个教程,感兴趣的同学可以到这里学习: https://segmentfault.com/ls/1...

查看原文

夜聆风 评论了文章 · 2018-09-21

三大图表库:ECharts 、 BizCharts 和 G2,该如何选择?

最近阿里正式开源的BizCharts图表库基于React技术栈,各个图表项皆采用了组件的形式,贴近React的使用特点。同时BizCharts基于G2进行封装,Bizcharts也继承了G2相关特性。公司目前统一使用的是ECharts图表库,下文将对3种图表库进行分析比对。

BizCharts

文档地址:BizCharts

一、安装

通过 npm/yarn 引入

npm install bizcharts --save

yarn add bizcharts  --save

二、引用

成功安装完成之后,即可使用 import 或 require 进行引用。

例子:

import { Chart, Geom, Axis, Tooltip, Legend } from 'bizcharts';
import chartConfig from './assets/js/chartConfig';

<div className="App">
    <Chart width={600} height={400} data={chartConfig.chartData} scale={chartConfig.cols}>
      <Axis name="genre" title={chartConfig.title}/>
      <Axis name="sold" title={chartConfig.title}/>
      <Legend position="top" dy={-20} />
      <Tooltip />
      <Geom type="interval" position="genre*sold" color="genre" />
    </Chart>
</div>


该示例中,图表的数据配置单独存入了其他js文件中,避免页面太过冗杂

module.exports = {
    chartData : [
        { genre: 'Sports', sold: 275, income: 2300 },
        { genre: 'Strategy', sold: 115, income: 667 },
        { genre: 'Action', sold: 120, income: 982 },
        { genre: 'Shooter', sold: 350, income: 5271 },
        { genre: 'Other', sold: 150, income: 3710 }
    ],
    // 定义度量
    cols : {
        sold: { alias: '销售量' }, // 数据字段别名映射
        genre: { alias: '游戏种类' }
    },
    title : {
        autoRotate: true, // 是否需要自动旋转,默认为 true
        textStyle: {
          fontSize: '12',
          textAlign: 'center',
          fill: '#999',
          fontWeight: 'bold',
          rotate: 30
        }, // 坐标轴文本属性配置
        position:'center', // 标题的位置,**新增**
    }
}

效果预览:
BizCharts示例

三、DataSet

BizCharts中可以通过dataset(数据处理模块)来对图标数据进行处理,该方法继承自G2,在下文中将对此进行详细分析。

快速跳转

G2

BizCharts基于G2进行开发,在研究BizCharts的过程中也一起对G2进行了实践。

一、安装

和BizCharts一样,可以通过 npm/yarn 引入

npm install @antv/g2 --save

yarn add @antv/g2 --save

与BizCharts不同,G2初始化数据并非以组件的形式引入,而是需要获取需要在某个DOM下初始化图表。获取该DOM的唯一属性id之后,通过chart()进行初始化。

二、引用

示例:

import React from 'react';
import G2 from '@antv/g2';
    class g2 extends React.Component {constructor(props) {
        super(props);
        this.state = {
          data :[
            { genre: 'Sports', sold: 275 },
            { genre: 'Strategy', sold: 115 },
            { genre: 'Action', sold: 120 },
            { genre: 'Shooter', sold: 350 },
            { genre: 'Other', sold: 150 }
          ]
        };
    }

    componentDidMount() {
        const chart = new G2.Chart({
          container: 'c1', // 指定图表容器 ID
          width: 600, // 指定图表宽度
          height: 300 // 指定图表高度
        });
        chart.source(this.state.data);
        chart.interval().position('genre*sold').color('genre');
        chart.render();
    }
    render() {
        return (
          <div id="c1" className="charts">
          </div>
        );
    }
}
export default g2;

效果图:
G2示例

三、DataSet

DataSet 主要有两方面的功能,解析数据(Connector)&加工数据(Transform)。

官方文档描述得比较详细,可以参考官网的分类:

源数据的解析,将csv, dsv,geojson 转成标准的JSON,查看Connector
加工数据,包括 filter,map,fold(补数据) 等操作,查看Transform
统计函数,汇总统计、百分比、封箱 等统计函数,查看 Transform
特殊数据处理,包括 地理数据、矩形树图、桑基图、文字云 的数据处理,查看 Transform
// step1 创建 dataset 指定状态量
const ds = new DataSet({
 state: {
    year: '2010'
 }
});

// step2 创建 DataView
const dv = ds.createView().source(data);

dv.transform({
 type: 'filter',
 callback(row) {
    return row.year === ds.state.year;
 }
});

// step3 引用 DataView
chart.source(dv);
// step4 更新状态量
ds.setState('year', '2012');

以下采用官网文档给出的示例进行分析

示例一

该表格里面的数据是美国各个州不同年龄段的人口数量,表格数据存放在类型为CVS的文件中
数据链接(该链接中为json类型的数据)

State小于5岁5至13岁14至17岁18至24岁25至44岁45至64岁65岁及以上
WY3825360890293145398013733814727965614
DC3635250439252257556919355714004370648
VT3263562538337576167915541918859386649
........................

初始化数据处理模块

import DataSet from '@antv/data-set';

const ds = new DataSet({
//state表示创建dataSet的状态量,可以不进行设置
 state: {
    currentState: 'WY'
    }
});

const dvForAll = ds
// 在 DataSet 实例下创建名为 populationByAge 的数据视图
    .createView('populationByAge') 
// source初始化图表数据,data可为http请求返回的数据结果
    .source(data, {
      type: 'csv', // 使用 CSV 类型的 Connector 装载 data,如果是json类型的数据,可以不进行设置,默认为json类型
});

/**
trnasform对数据进行加工处理,可通过type设置加工类型,具体参考上文api文档
加工过后数据格式为
[
{state:'WY',key:'小于5岁',value:38253},
{state:'WY',key:'5至13岁',value:60890},
]
*/ 
dvForAll.transform({
    type: 'fold',
    fields: [ '小于5岁','5至13岁','14至17岁','18至24岁','25至44岁','45至64岁','65岁及以上' ],
    key: 'age',
     value: 'population'
});

//其余transform操作
const dvForOneState = ds
    .createView('populationOfOneState')
    .source(dvForAll); // 从全量数据继承,写法也可以是.source('populationByAge')
 dvForOneState
     .transform({ // 过滤数据,筛选出state符合的地区数据
    type: 'filter',
    callback(row) {
      return row.state === ds.state.currentState;
    }
})
 .transform({
    type: 'percent',
    field: 'population',
    dimension: 'age',
    as: 'percent'
    });

使用G2绘图
G2-chart Api文档

import G2 from '@antv/g2';

// 初始化图表,id指定了图表要插入的dom,其他属性设置了图表所占的宽高
const c1 = new G2.Chart({
  id: 'c1',
  forceFit: true,
  height: 400,
});

// chart初始化加工过的数据dvForAll
c1.source(dvForAll);

// 配置图表图例
c1.legend({
  position: 'top',
});

// 设置坐标轴配置,该方法返回 chart 对象,以下代码表示将坐标轴属性为人口的数据,转换为M为单位的数据
c1.axis('population', {
  label: {
    formatter: val => {
      return val / 1000000 + 'M';
    }
  }
});

c1.intervalStack()
  .position('state*population')
  .color('age')
  .select(true, {
    mode: 'single',
    style: {
      stroke: 'red',
      strokeWidth: 5
    }
  });
  
//当tooltip发生变化的时候,触发事件,修改ds的state状态量,一旦状态量改变,就会触发图表的更新,所以c2饼图会触发改变
c1.on('tooltip:change', function(evt) {
  const items = evt.items || [];
  if (items[0]) {
  //修改的currentState为鼠标所触及的tooltip的地区
    ds.setState('currentState', items[0].title);
  }
});

// 绘制饼图
const c2 = new G2.Chart({
  id: 'c2',
  forceFit: true,
  height: 300,
  padding: 0,
});
c2.source(dvForOneState);
c2.coord('theta', {
  radius: 0.8 // 设置饼图的大小
});
c2.legend(false);
c2.intervalStack()
  .position('percent')
  .color('age')
  .label('age*percent',function(age, percent) {
    percent = (percent * 100).toFixed(2) + '%';
    return age + ' ' + percent;
  });

c1.render();
c2.render();

ECharts

ECharts是一个成熟的图表库, 使用方便、图表种类多、容易上手。文档资源也比较丰富,在此不做赘述。
ECharts文档

ECharts & BizCharts & G2 对比

对比BizCharts和G2两种图表库,BizCharts主要是进行了一层封装,使得图表可以以组件的形式进行调用,按需加载,使用起来更加方便。
简单对比一下三个图表库的区别:

初始化图表:
ECharts:

// 基于准备好的dom,初始化ECharts实例
var myChart = echarts.init(document.getElementById('main'));

BizCharts:

// 以组件的形式,组合调用
import { Chart, Geom, Axis, ... } from 'bizcharts';

<Chart width={600} height={400} data={data}>
    ...
</Chart>

G2:

// 基于准备好的dom,配置之后进行初始化
const chart = new G2.Chart({
    container: 'c1', // 指定图表容器 ID
    width: 600, // 指定图表宽度
    height: 300 // 指定图表高度
});
chart.source(data);
chart.render();
 
<div id="c1" className="charts"></div>

配置:

ECharts:

// 集中在options中进行配置
myChart.setOption({
    title: {
        ...
    },
    tooltip: {},
    xAxis: {
        data: [...]
    },
    yAxis: {},
    series: [{
        ...
    }]
});

BizCharts:

// 根据组件需要,配置参数之后进行赋值
const cols = {...};
const data = {...};
<Chart width={600} height={400} data={data} sca`enter code here`le={cols}>
    ...
</Chart>

G2:

chart.tooltip({
  triggerOn: '...'
  showTitle: {boolean}, // 是否展示 title,默认为 true
  crosshairs: {
    ...
    style: {
      ...
    }
  }
});

事件:

ECharts:事件 api文档

myChart.on('click', function (params) {
    console.log(params);
});

BizCharts:事件 api文档

<chart
  onEvent={e => {
    //do something
  }}
/>

G2: 事件 api文档

chart.on('mousedown', ev => {});

总结

对比以上3种图表,ECharts和BizCharts相对容易使用,尤其ECharts的配置非常清晰,BizCharts与其也有一定相似之处。BizCharts优势在于组件化的形式使得dom结构相对清晰,按需引用。G2比较适合需要大量图表交互时引用,其丰富的api处理交互逻辑相对更有优势。

广而告之

本文发布于薄荷前端周刊,欢迎Watch & Star ★,转载请注明出处。

欢迎讨论,点个赞再走吧 。◕‿◕。 ~

查看原文

夜聆风 评论了文章 · 2018-07-26

10分钟上手最新vue脚手架

vue-cli 3.0 入门介绍

环境安装

全新版本的脚手架、逼格非常高、 记住这个名字 @vue/cli ,对就是这个 你npm 或者yarn 安装就行了,先保证全局环境有它。


   npm install -g @vue/cli

   yarn add global @vue/cli

创建项目

这里对比下以前2.X之前的版本 ,新版本把插件以及模板等 移植到命令行界面了.

旧版创建命令
2.xvue init <template-name> <project-name>
3.xvue create <project-name>

来一张图把 ,这里已经有几个默认配好的模板了,我们选最后的Manually select features

vue-cli3.0在你创建后会有一个保存当前配置的功能

clipboard.png

配置项目插件和功能

这里就很傻瓜了, 你要集成什么 就选就行了。我这里选个我比较常用的。

  • TypeScript
  • PWA
  • Vue-router
  • Vuex
  • CSS预处理
  • eslint prettier
  • 自动化测试单元测试 、e2e

clipboard.png

这里我选LESS

clipboard.png

这里我选eslint + prettier
clipboard.png

这里选择语法检查的方式 保存就检查 还是fix和commit时候检查,我就默认选第一个了

clipboard.png

这里单元测试 插件我选jest
clipboard.png

这里是把babel,postcss,eslint这些配置文件放哪

  1. 独立文件放置
  2. package.json

个人喜好 这里我独立放
clipboard.png

最后就是选择 是否记录一下? 下次继续使用这套配置 ,这里咱就不存了 这玩呢存多了 我都不知道怎么删 知道的小哥哥小姐姐麻烦 告诉我下哈。
clipboard.png

ok最后确定后 等待装好吧

clipboard.png

嗖 装好了

clipboard.png

启动项目

  1. 进入目录,启动项目 这里 vue-cli 3.x 默认会打开浏览器 地址也会打在控制台。
   
   yarn serve 
   // OR
   npm run serve

clipboard.png

启动后的界面就不截图了 ,按步骤正常操作下来应该跟之前版本一样。

项目分析

首先看下整体目录 比 2.x之前 是精简了不少

clipboard.png

去掉了2.xbuildconfig等目录 ,大部分配置 都集成到vue.config.js这里了

vue.config.js里
大概包括了配置 常用的输出路径名、跟目录、预处理、devServer配置、pwa、dll、第三方插件等等
详细配置可以看官方文档 详细config配置

如何随心所欲

1. 服务器配置修改

这里我先改个端口, 修改vue.config.js 然后重新启动工程 , 可以看到已经改成5999端口了

module.exports = {
  lintOnSave: false,
  devServer: {
    port: 5999
  }
}

2. 常用webpack配置修改

webpack的配置在这个属性里修改configureWebpack  

包括plugins也可以自己扩展 ,本身尤大已经把常用的都封装了 ,不满足可以自行扩展。


这里改个webpack devtool输出方式、默认那个我属实不知道怎么跟踪代码

configureWebpack: config => {
    if (process.env.NODE_ENV === 'development') {
        config.devtool = 'source-map'
        // mutate config for production...
    } 
}

其他配置 就不一一介绍了 具体可以看这里webpack

3. 全局变量的设置

在项目根目录 创建二个文件

.env.development
.env.production

里面配置键值对就行了

但要注意 这里必须以VUE_APP开头

这样我们就可以自定义个全局变量在某个模式下

    VUE_APP_MOCK_URL = 'http://xxxx.xxx.xx.xx/mockjs/'

比如这样在axios中就可以配置根路径了


const service = axios.create({
    baseURL: process.env.VUE_APP_MOCK_URL
})

总结

本文使用vue-cli3.x 从环境、到创建、到配置、和常用项目技巧进行了简单介绍,希望可以帮助到刚使用的人 。

参考文献资料
作者github

查看原文

夜聆风 回答了问题 · 2018-07-06

vue中对后台返回字段判断是否写入模板中

用computed 计算属性

关注 5 回答 4

夜聆风 回答了问题 · 2018-07-06

vuex,如何调用modules里面定义的mutations

一般state的状态 要通过getter 以模块的命名空间获取

import { mapGetters } from 'vuex

// vue组件
computed:{
   ...mapGetters({
        a: 'a/a'
   })
}

关注 5 回答 4

夜聆风 评论了文章 · 2018-07-06

10分钟上手最新vue脚手架

vue-cli 3.0 入门介绍

环境安装

全新版本的脚手架、逼格非常高、 记住这个名字 @vue/cli ,对就是这个 你npm 或者yarn 安装就行了,先保证全局环境有它。


   npm install -g @vue/cli

   yarn add global @vue/cli

创建项目

这里对比下以前2.X之前的版本 ,新版本把插件以及模板等 移植到命令行界面了.

旧版创建命令
2.xvue init <template-name> <project-name>
3.xvue create <project-name>

来一张图把 ,这里已经有几个默认配好的模板了,我们选最后的Manually select features

vue-cli3.0在你创建后会有一个保存当前配置的功能

clipboard.png

配置项目插件和功能

这里就很傻瓜了, 你要集成什么 就选就行了。我这里选个我比较常用的。

  • TypeScript
  • PWA
  • Vue-router
  • Vuex
  • CSS预处理
  • eslint prettier
  • 自动化测试单元测试 、e2e

clipboard.png

这里我选LESS

clipboard.png

这里我选eslint + prettier
clipboard.png

这里选择语法检查的方式 保存就检查 还是fix和commit时候检查,我就默认选第一个了

clipboard.png

这里单元测试 插件我选jest
clipboard.png

这里是把babel,postcss,eslint这些配置文件放哪

  1. 独立文件放置
  2. package.json

个人喜好 这里我独立放
clipboard.png

最后就是选择 是否记录一下? 下次继续使用这套配置 ,这里咱就不存了 这玩呢存多了 我都不知道怎么删 知道的小哥哥小姐姐麻烦 告诉我下哈。
clipboard.png

ok最后确定后 等待装好吧

clipboard.png

嗖 装好了

clipboard.png

启动项目

  1. 进入目录,启动项目 这里 vue-cli 3.x 默认会打开浏览器 地址也会打在控制台。
   
   yarn serve 
   // OR
   npm run serve

clipboard.png

启动后的界面就不截图了 ,按步骤正常操作下来应该跟之前版本一样。

项目分析

首先看下整体目录 比 2.x之前 是精简了不少

clipboard.png

去掉了2.xbuildconfig等目录 ,大部分配置 都集成到vue.config.js这里了

vue.config.js里
大概包括了配置 常用的输出路径名、跟目录、预处理、devServer配置、pwa、dll、第三方插件等等
详细配置可以看官方文档 详细config配置

如何随心所欲

1. 服务器配置修改

这里我先改个端口, 修改vue.config.js 然后重新启动工程 , 可以看到已经改成5999端口了

module.exports = {
  lintOnSave: false,
  devServer: {
    port: 5999
  }
}

2. 常用webpack配置修改

webpack的配置在这个属性里修改configureWebpack  

包括plugins也可以自己扩展 ,本身尤大已经把常用的都封装了 ,不满足可以自行扩展。


这里改个webpack devtool输出方式、默认那个我属实不知道怎么跟踪代码

configureWebpack: config => {
    if (process.env.NODE_ENV === 'development') {
        config.devtool = 'source-map'
        // mutate config for production...
    } 
}

其他配置 就不一一介绍了 具体可以看这里webpack

3. 全局变量的设置

在项目根目录 创建二个文件

.env.development
.env.production

里面配置键值对就行了

但要注意 这里必须以VUE_APP开头

这样我们就可以自定义个全局变量在某个模式下

    VUE_APP_MOCK_URL = 'http://xxxx.xxx.xx.xx/mockjs/'

比如这样在axios中就可以配置根路径了


const service = axios.create({
    baseURL: process.env.VUE_APP_MOCK_URL
})

总结

本文使用vue-cli3.x 从环境、到创建、到配置、和常用项目技巧进行了简单介绍,希望可以帮助到刚使用的人 。

参考文献资料
作者github

查看原文

夜聆风 赞了文章 · 2018-06-15

打包工具的配置教程见的多了,但它们的运行原理你知道吗?

clipboard.png

前端模块化成为了主流的今天,离不开各种打包工具的贡献。社区里面对于webpack,rollup以及后起之秀parcel的介绍层出不穷,对于它们各自的使用配置分析也是汗牛充栋。为了避免成为一位“配置工程师”,我们需要来了解一下打包工具的运行原理,只有把核心原理搞明白了,在工具的使用上才能更加得心应手。

本文基于parcel核心开发者@ronami的开源项目minipack而来,在其非常详尽的注释之上加入更多的理解和说明,方便读者更好地理解。

1、打包工具核心原理

顾名思义,打包工具就是负责把一些分散的小模块,按照一定的规则整合成一个大模块的工具。与此同时,打包工具也会处理好模块之间的依赖关系,最终这个大模块将可以被运行在合适的平台中。

打包工具会从一个入口文件开始,分析它里面的依赖,并且再进一步地分析依赖中的依赖,不断重复这个过程,直到把这些依赖关系理清挑明为止。

从上面的描述可以看到,打包工具最核心的部分,其实就是处理好模块之间的依赖关系,而minipack以及本文所要讨论的,也是集中在模块依赖关系的知识点当中。

为了简单起见,minipack项目直接使用ES modules规范,接下来我们新建三个文件,并且为它们之间建立依赖:

/* name.js */

export const name = 'World'
/* message.js */

import { name } from './name.js'

export default `Hello ${name}!`
/* entry.js */

import message from './message.js'

console.log(message)

它们的依赖关系非常简单:entry.jsmessage.jsname.js,其中entry.js将会成为打包工具的入口文件。

但是,这里面的依赖关系只是我们人类所理解的,如果要让机器也能够理解当中的依赖关系,就需要借助一定的手段了。

2、依赖关系解析

新建一个js文件,命名为minipack.js,首先引入必要的工具。

/* minipack.js */

const fs = require('fs')
const path = require('path')
const babylon = require('babylon')
const traverse = require('babel-traverse').default
const { transformFromAst } = require('babel-core')

接下来,我们会撰写一个函数,这个函数接收一个文件作为模块,然后读取它里面的内容,分析出其所有的依赖项。当然,我们可以通过正则匹配模块文件里面的import关键字,但这样做非常不优雅,所以我们可以使用babylon这个js解析器把文件内容转化成抽象语法树(AST),直接从AST里面获取我们需要的信息。

得到了AST之后,就可以使用babel-traverse去遍历这棵AST,获取当中关键的“依赖声明”,然后把这些依赖都保存在一个数组当中。

最后使用babel-coretransformFromAst方法搭配babel-preset-env插件,把ES6语法转化成浏览器可以识别的ES5语法,并且为该js模块分配一个ID。

let ID = 0

function createAsset (filename) {
  // 读取文件内容
  const content = fs.readFileSync(filename, 'utf-8')

  // 转化成AST
  const ast = babylon.parse(content, {
    sourceType: 'module',
  });

  // 该文件的所有依赖
  const dependencies = []

  // 获取依赖声明
  traverse(ast, {
    ImportDeclaration: ({ node }) => {
      dependencies.push(node.source.value);
    }
  })

  // 转化ES6语法到ES5
  const {code} = transformFromAst(ast, null, {
    presets: ['env'],
  })

  // 分配ID
  const id = ID++

  // 返回这个模块
  return {
    id,
    filename,
    dependencies,
    code,
  }
}

运行createAsset('./example/entry.js'),输出如下:

{ id: 0,
  filename: './example/entry.js',
  dependencies: [ './message.js' ],
  code: '"use strict";\n\nvar _message = require("./message.js");\n\nvar _message2 = _interopRequireDefault(_message);\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }\n\nconsole.log(_message2.default);' }

可见entry.js文件已经变成了一个典型的模块,且依赖已经被分析出来了。接下来我们就要递归这个过程,把“依赖中的依赖”也都分析出来,也就是下一节要讨论的建立依赖关系图集。

3、建立依赖关系图集

新建一个名为createGragh()的函数,传入一个入口文件的路径作为参数,然后通过createAsset()解析这个文件使之定义成一个模块。

接下来,为了能够挨个挨个地对模块进行依赖分析,所以我们维护一个数组,首先把第一个模块传进去并进行分析。当这个模块被分析出还有其他依赖模块的时候,就把这些依赖模块也放进数组中,然后继续分析这些新加进去的模块,直到把所有的依赖以及“依赖中的依赖”都完全分析出来。

与此同时,我们有必要为模块新建一个mapping属性,用来储存模块、依赖、依赖ID之间的依赖关系,例如“ID为0的A模块依赖于ID为2的B模块和ID为3的C模块”就可以表示成下面这个样子:

{
  0: [function A () {}, { 'B.js': 2, 'C.js': 3 }]
}

搞清楚了个中道理,就可以开始编写函数了。

function createGragh (entry) {
  // 解析传入的文件为模块
  const mainAsset = createAsset(entry)
  
  // 维护一个数组,传入第一个模块
  const queue = [mainAsset]

  // 遍历数组,分析每一个模块是否还有其它依赖,若有则把依赖模块推进数组
  for (const asset of queue) {
    asset.mapping = {}
    // 由于依赖的路径是相对于当前模块,所以要把相对路径都处理为绝对路径
    const dirname = path.dirname(asset.filename)
    // 遍历当前模块的依赖项并继续分析
    asset.dependencies.forEach(relativePath => {
      // 构造绝对路径
      const absolutePath = path.join(dirname, relativePath)
      // 生成依赖模块
      const child = createAsset(absolutePath)
      // 把依赖关系写入模块的mapping当中
      asset.mapping[relativePath] = child.id
      // 把这个依赖模块也推入到queue数组中,以便继续对其进行以来分析
      queue.push(child)
    })
  }

  // 最后返回这个queue,也就是依赖关系图集
  return queue
}

可能有读者对其中的for...of ...循环当中的queue.push有点迷,但是只要尝试过下面这段代码就能搞明白了:

var numArr = ['1', '2', '3']

for (num of numArr) {
  console.log(num)
  if (num === '3') {
    arr.push('Done!')
  }
}

尝试运行一下createGraph('./example/entry.js'),就能够看到如下的输出:

[ { id: 0,
    filename: './example/entry.js',
    dependencies: [ './message.js' ],
    code: '"use strict";\n\nvar _message = require("./message.js");\n\nvar _message2 = _interopRequireDefault(_message);\n\nfunction _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }\n\nconsole.log(_message2.default);',
    mapping: { './message.js': 1 } },
  { id: 1,
    filename: 'example/message.js',
    dependencies: [ './name.js' ],
    code: '"use strict";\n\nObject.defineProperty(exports, "__esModule", {\n  value: true\n});\n\nvar _name = require("./name.js");\n\nexports.default = "Hello " + _name.name + "!";',
    mapping: { './name.js': 2 } },
  { id: 2,
    filename: 'example/name.js',
    dependencies: [],
    code: '"use strict";\n\nObject.defineProperty(exports, "__esModule", {\n  value: true\n});\nvar name = exports.name = \'world\';',
    mapping: {} } ]

现在依赖关系图集已经构建完成了,接下来就是把它们打包成一个单独的,可直接运行的文件啦!

4、进行打包

上一步生成的依赖关系图集,接下来将通过CommomJS规范来实现加载。由于篇幅关系,本文不对CommomJS规范进行扩展,有兴趣的读者可以参考@阮一峰 老师的一篇文章《浏览器加载 CommonJS 模块的原理与实现》,说得非常清晰。简单来说,就是通过构造一个立即执行函数(function () {})(),手动定义moduleexportsrequire变量,最后实现代码在浏览器运行的目的。

接下来就是依据这个规范,通过字符串拼接去构建代码块。

function bundle (graph) {
  let modules = ''

  graph.forEach(mod => {
    modules += `${mod.id}: [
      function (require, module, exports) { ${mod.code} },
      ${JSON.stringify(mod.mapping)},
    ],`
  })

  const result = `
    (function(modules) {
      function require(id) {
        const [fn, mapping] = modules[id];

        function localRequire(name) {
          return require(mapping[name]);
        }

        const module = { exports : {} };

        fn(localRequire, module, module.exports);

        return module.exports;
      }

      require(0);
    })({${modules}})
  `
  return result
}

最后运行bundle(createGraph('./example/entry.js')),输出如下:

(function (modules) {
  function require(id) {
    const [fn, mapping] = modules[id];

    function localRequire(name) {
      return require(mapping[name]);
    }

    const module = { exports: {} };

    fn(localRequire, module, module.exports);

    return module.exports;
  }

  require(0);
})({
  0: [
    function (require, module, exports) {
      "use strict";

      var _message = require("./message.js");

      var _message2 = _interopRequireDefault(_message);

      function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }

      console.log(_message2.default);
    },
    { "./message.js": 1 },
  ], 1: [
    function (require, module, exports) {
      "use strict";

      Object.defineProperty(exports, "__esModule", {
        value: true
      });

      var _name = require("./name.js");

      exports.default = "Hello " + _name.name + "!";
    },
    { "./name.js": 2 },
  ], 2: [
    function (require, module, exports) {
      "use strict";

      Object.defineProperty(exports, "__esModule", {
        value: true
      });
      var name = exports.name = 'world';
    },
    {},
  ],
})

这段代码将能够直接在浏览器运行,输出“Hello world!”。

至此,整一个打包工具已经完成。

5、归纳总结

经过上面几个步骤,我们可以知道一个模块打包工具,第一步会从入口文件开始,对其进行依赖分析,第二步对其所有依赖再次递归进行依赖分析,第三步构建出模块的依赖图集,最后一步根据依赖图集使用CommonJS规范构建出最终的代码。明白了当中每一步的目的,便能够明白一个打包工具的运行原理。

最后再次感谢@ronami的开源项目minipack,其源码有着更为详细的注释,非常值得大家阅读。

查看原文

赞 100 收藏 128 评论 0

夜聆风 回答了问题 · 2018-05-21

解决诚意求教:关于前端/数据分析求职的一些问题

准备好简历 去投心仪的公司吧
放松心态 加油

关注 11 回答 12

夜聆风 回答了问题 · 2018-05-18

echarts中grid的这个设置怎么理解?

grid 就是视区的比例 ,图表可视区距离四边容器的距离百分比/像素值

数组这种无非映射每个series坐标系的index ,series有位置设置

关注 2 回答 1

夜聆风 回答了问题 · 2018-04-02

解决echarts 怎么点击图例显示该折线,隐藏其他折线

看看 事件那块的API 类似这个legendselectchanged

关注 4 回答 3

认证与成就

  • 获得 132 次点赞
  • 获得 39 枚徽章 获得 3 枚金徽章, 获得 14 枚银徽章, 获得 22 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2015-04-17
个人主页被 1.9k 人浏览