关于前端工程化:大前端2022版全面升级完结

download:大前端2022版全面降级完结大前端:逾越界线的技术大前端是一个宽泛的概念,它波及到多个技术畛域和平台。传统意义上,前端指的是网站和应用程序中用户能够看到和与之交互的局部。然而,随着挪动技术的倒退,前端技术曾经扩大到了挪动应用程序、桌面应用程序和物联网等畛域,这就是大前端。 大前端的演变历程在过来的几年里,前端技术始终在迅速倒退。随着 HTML5 和 CSS3 的遍及,前端开发者取得了更多的自由度和创造力。同时,JavaScript 也变得更加弱小,当初能够用于构建残缺的应用程序,而不仅仅是简略的网页。 随着挪动设施的遍及,前端开发者的工作范畴也扩大到了挪动利用程序开发。React Native 和 Flutter 是两个风行的跨平台开发框架,它们使得前端开发者能够应用雷同的技能开发 iOS 和 Android 应用程序。 物联网是另一个须要前端技术的畛域。当初,前端开发者能够应用 JavaScript 管制嵌入式设施,并构建物联网应用程序。例如,应用 Raspberry Pi 和 Arduino 等设施能够构建智能家居零碎、传感器网络和机器人等。 大前端的技术栈大前端的技术栈十分宽泛,涵盖了多个畛域。以下是一些罕用的技术: 前端开发前端开发包含 HTML、CSS 和 JavaScript,这些技术用于创立网站和应用程序的用户界面。React、Vue 和 Angular 是风行的 JavaScript 框架,它们能够帮忙前端开发者组织代码并进步开发效率。 挪动利用程序开发挪动利用程序开发须要对 iOS 和 Android 等平台有深刻的理解。React Native 和 Flutter 是两个跨平台开发框架,它们能够帮忙前端开发者应用雷同的技能在多个平台上构建应用程序。 物联网开发物联网开发须要前端开发者相熟嵌入式设施和无线通信技术。Node.js 能够用于管制嵌入式设施,而 MQTT 和 CoAP 等协定能够用于设施之间的通信。 大前端的将来随着新技术的涌现,大前端将持续扩大其技术栈和利用范畴。WebAssembly 是一个新兴的技术,它容许应用 C 和 C++ 等语言编写高性

June 2, 2023 · 1 min · jiezi

关于前端工程化:React组件库实践React-Typescript-Less-Rollup-Storybook

背景原先在做低代码平台的时候,刚好有搭载React组件库的需要,所以就搞了一套通用的React组件库模版。目前通过这套模板也搭建过好几个组件库。 为了让这个模板更洁净和通用,我把所有和低代码相干的代码都剔除了,只保留最纯正的react组件库所须要的代码,并且同步到了github:react-standard-components。 技术栈技术栈:React + Typescript + Less + Rollup + Storybook 组件库用Rollup.js构建,目前反对2种format:umd、esm。目前次要的应用形式还是CDN和es module,所以就没构建cjs格局的bundle。 款式用style inject形式解决,所以不存在独自的css产物。引入组件时也不须要额定引入.css文件,间接引入组件bundle即可。 反对alias,我发现很多react组件库相干的文章或者仓库,其所提供的相干配置都不反对alias,但个人感觉alias是能晋升开发体验的,所以特地提一下。 默认别名有:"@/*": ["src/*"],可自行批改。须要留神的是Storybook有本人的配置在.storybook/main.js,如果要改的话,这里storybook的webpack配置也须要改。 介绍RollupRollup.js 是一个 JavaScript 模块打包器,能够将多个模块打包成单个文件,以便于在浏览器中应用。与其余打包器相比,Rollup.js 的独特之处在于它反对 ES6 模块语法,并能将其转换成实用于浏览器的代码,同时还可进行 tree shaking,即只打包应用到的代码,不打包未应用的代码,从而减小打包后文件的体积。 Rollup.js 的另一大劣势是反对插件机制,能够通过插件扩大其性能。例如,能够应用插件将 CSS 文件打包进 JavaScript 文件中,或者应用插件将 TypeScript 转换成 JavaScript。 Rollup.js 的应用也非常简单。首先,须要在我的项目中装置 Rollup.js,并创立一个配置文件,指定入口文件和输入文件的门路,以及须要应用的插件。而后,在命令行中执行打包命令,即可生成打包后的文件。 Rollup.js 是一个功能强大、易于应用的 JavaScript 模块打包器,实用于各种规模的我的项目,能够帮忙开发者进步代码的性能和可维护性。 TypescriptTypeScript是一种开源的编程语言,它是JavaScript的一个超集,由Microsoft开发和保护。它增加了动态类型检查和其余语言个性,以进步JavaScript代码的可读性、可维护性和可扩展性。 与JavaScript相比,TypeScript提供了更好的类型反对,这意味着开发人员能够在编码时发现并解决类型相干的谬误。此外,TypeScript还反对类、接口、泛型、命名空间等常见的面向对象编程个性,这些个性能够使代码更加模块化和可重用。 TypeScript还具备一些高级性能,例如枚举、元组、穿插类型、联结类型等,这些性能能够在编写大型应用程序时进步代码的可读性和可维护性。 TypeScript能够应用任何古代的JavaScript库和框架,因为它是JavaScript的超集,能够编译成纯JavaScript并在任何反对JavaScript的浏览器或运行时环境中运行。 TypeScript是一种功能强大、可扩大的编程语言,它提供了更好的类型反对和面向对象编程个性,能够帮忙开发人员编写更可读、可保护和可扩大的JavaScript代码。 LessLess是一种基于CSS的预处理器,它能够帮忙开发人员更加高效地编写CSS代码。Less应用一种相似于CSS的语法,同时减少了一些额定的个性,如嵌套规定、变量、混合等,使得CSS的编写更加简洁、易读、易保护。通过应用Less,开发人员能够更加灵便地组织和治理CSS代码,同时也能够进步开发效率。 Less能够通过两种形式应用:一种是将Less编译为CSS,而后在网页中引入CSS文件;另一种是在网页中间接引入Less,而后通过JavaScript动静地生成CSS代码。这两种形式都有各自的优缺点,开发人员能够依据本人的需要和习惯进行抉择。 Less还有一些其余的个性,如函数、运算符、条件语句等,这些个性能够让开发人员更加灵便地解决CSS代码。同时,Less还反对插件机制,能够通过插件来扩大Less的性能。 StorybookStorybook 是一个为 React、Vue、Angular 等各种前端框架提供 UI 组件开发环境的工具,能够帮忙开发者以独立的形式开发、测试和展现 UI 组件,进步开发效率和可复用性。 Storybook 的次要性能是将 UI 组件与其不同状态下的展现成果以故事(Story)的模式出现进去,不便开发者进行交互式开发和调试。开发者能够在 Storybook 中创立不同的故事,每个故事对应一个特定的组件状态,例如不同的 props、不同的交互等等,并在 Storybook 中展现进去,不便开发者进行测试和交互式开发。 除了展现组件之外,Storybook 还提供了各种插件和工具,帮忙开发者进行疾速开发和调试。例如,能够应用插件将 UI 组件文档化,或者应用插件将组件库公布到 NPM 中。 ...

April 3, 2023 · 1 min · jiezi

关于前端工程化:helmicro-模块联邦新革命实战预告

hel-micro git:https://github.com/tnfe/hel (你的小星星是咱们开源的最大能源) bilibili 模块联邦分享内容预报 在线案例体验:https://codesandbox.io/s/hel-... (main文件批改版本号即拉取不同版本的代码) why 背景模块联邦是一个最近两年诞生的一门新技术,它的平凡之处是在放弃以后前端开发模块化、组件化、工程化的高效率体系下,容许模块独立开发、独立部署,通过 CDN 间接共享,从而挣脱npm包体无奈动静更新的枷锁,从而推动整个前端界开发和运行体验回升到一个新高度。 而隔离性强的微容器 + 灵便度更高的微模块联合起来做微前端架构必将成为支流 ,微模块技术目前有以编译时插件注入的 webpack 5 module federation、vite-plugin-federation,运行时注入的 hel-micro,而 hel-micro 对编译时插件类技术将会是降维打击 ,hel-micro 以js为基建做近程模块散发体系,而非打包工具插件,将彻底辞别工具链绑定这个致命问题,意味着你无论是 webpack 2 3 4 vite parcel or 其余任何体系,都能够享受模块联邦带来的免构建、热更新、动静共享劣势. 分享工夫

August 30, 2022 · 1 min · jiezi

关于前端工程化:在-Web-应用的运行时实现多分支并存和切换

背景一般来说,SaaS 服务商提供的是标准化的产品服务,体现的是所有客户的共性需要。然而,局部客户(尤其是大客户),会提出性能、UI 等方面的定制需要。针对这些定制需要,大体上有两个解决方案。 第一个计划是提供应用程序 SDK,由客户的开发团队实现整个定制利用的开发和部署,SaaS 服务商提供必要的技术支持即可。此计划要求客户的开发团队具备较强的 IT 业余能力。 第二个计划则是由 SaaS 服务商的开发团队在 SaaS 利用的根底上进行二次开发,并部署。此计划次要面向 IT 业余能力较弱,或者仅需在 SaaS 利用的根底上进行大量定制的客户。然而,要反对这种定制形式,相当于要求 SaaS 服务商在同一个利用中,针对不同的客户运行不同分支的代码。要达到这个目标,应用程序的架构也要进行相应的革新。本文次要讲述革新的计划及其代码实现。 计划概览对于前后端拆散的我的项目来说,通过构建,最终会生成 html、js、css 三种代码文件。以基于 Vue.js 框架的我的项目为例,其构建进去的 index.html,内容与上面的代码类似: <!DOCTYPE html><html><head> <meta charset="utf-8"> <link href="https://cdn.my-app.net/sample/assets/css/chunk-0c7134a2.11fa7980.css" rel="prefetch"> <link href="https://cdn.my-app.net/sample/assets/js/chunk-0c7134a2.02a43289.js" rel="prefetch"> <link href="https://cdn.my-app.net/sample/assets/css/app.2dd9bc59.css" rel="preload" as="style"> <link href="https://cdn.my-app.net/sample/assets/js/vendors~app.f1dba939.js" rel="preload" as="script"> <link href="https://cdn.my-app.net/sample/assets/js/app.f7eb55ca.js" rel="preload" as="script"> <link href="https://cdn.my-app.net/sample/assets/css/app.2dd9bc59.css" rel="stylesheet"> </head> <body> <div id="app"></div> <script src="https://cdn.my-app.net/sample/assets/js/vendors~app.f1dba939.js"></script> <script src="https://cdn.my-app.net/sample/assets/js/app.f7eb55ca.js"></script> </body> </html>实际上,index.html 只是拜访入口,次要作用就是加载 css 和 js 资源。换句话说:任何的 html 页面,只有加载了上述 css 和 js 资源,都能够运行这个利用。 既然如此,只有做一个利用入口页,并依据客户配置加载相应代码分支构建进去的 css 和 js 资源即可。整体流程如下图所示: 构建计划入口页要加载对应分支的 css 和 js 资源,首先须要一个资源列表。咱们能够在构建流程减少一个步骤,把 js 和 css 的援用提取到一个资源目录文件(index-assets.json)中: const fs = require('fs');const content = fs.readFileSync('./dist/index.html', 'utf-8');// 匹配 html 中的 js 或 css 援用标签const assetTags = content.match(/<(?:link|script).*?>/gi) || [];let result = [];assetTags.forEach((assetTag) => { const asset = { tagName: '', attrs: {} }; // 解析标签名 if (/<(\w+)/.test(assetTag)) { asset.tagName = RegExp.$1; } // 解析属性 const reAttrs = /\s(\w+)=["']?([^\s<>'"]+)/gi; let attr; while ((attr = reAttrs.exec(assetTag)) !== null) { asset.attrs[attr[1]] = attr[2]; } result.push(asset);});// 移除 preload 的资源,并把 prefetch 的资源放到 result 的最初面const prefetches = [];for (let i = 0, item; i < result.length;) { item = result[i]; if (item.tagName === 'link') { if (item.attrs.rel === 'preload') { result.splice(i, 1); continue; } else if (item.attrs.rel === 'prefetch') { prefetches.push(result.splice(i, 1)[0]); continue; } } i++;}result = result.concat(prefetches);fs.writeFileSync( './dist/index-assets.json', JSON.stringify({ list: result }), 'utf-8');执行脚本后,就会生成资源目录文件,其内容为: ...

August 9, 2022 · 3 min · jiezi

关于前端工程化:大前端2022版全面升级完结无密内置文档资料

download:大前端2022版全面降级完结无密内置文档资料FutureTask源码深度剖析在JDK的FutureTask当中会使用到一个工具LockSupport,在正式介绍FutureTask之前咱们先熟悉一下这个工具。LockSupport次要是用于阻塞和唤醒线程的,它次要是通过包装UnSafe类,通过UnSafe类当中的方法进行实现的,他底层的方法是通过依赖JVM实现的。在LockSupport当中次要有以下三个方法: unpark(Thread thread))方法,这个方法可能给线程thread发放一个许可证,你可能通过多次调用这个方法给线程发放许可证,每次调用都会给线程发放一个许可证,然而这个许可证不能够进行累计,也就是说一个线程能够具备的最大的许可证的个数是1一个。 park()方法,这个线程会生产调用这个方法的线程一个许可证,因为线程的默认许可证的个数是0,如果调用一次那么许可证的数目就变成-1,当许可证的数目小于0的时候线程就会阻塞,因此如果线程从来没用调用unpark方法的话,那么在调用这个方法的时候会阻塞,如果线程在调用park方法之前,有线程调用unpark(thread)方法,给这个线程发放一个许可证的话,那么调用park方法就不会阻塞。 parkNanos(long nanos)方法,同park方法一样,nanos示意最长阻塞超时工夫,超时后park方法将主动返回,如果调用这个方法的线程有许可证的话也不会阻塞。 import java.util.concurrent.TimeUnit;import java.util.concurrent.locks.LockSupport; public class Demo { public static void main(String[] args) throws InterruptedException { Thread thread = new Thread(() -> { LockSupport.park(); // 没有许可证 阻塞住这个线程 try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("阻塞实现");});thread.start();TimeUnit.SECONDS.sleep(2);LockSupport.unpark(thread); //给线程 thread 发放一个许可证System.out.println("线程启动");}} 复制代码下面代码的执行后果线程启动阻塞实现复制代码从下面代码咱们可能知道LockSupport.park()可能阻塞一个线程,因为如果没有阻塞的话必定会先打印阻塞实现,因为打印这句话的线程只休眠一秒,主线程休眠两秒。在源代码当中你可能会遇到UNSAFE.compareAndSwapXXX的代码,这行代码次要是进行原子交换操作CAS,比如:UNSAFE.compareAndSwapInt(this, stateOffset, NEW, CANCELLED)))复制代码下面的代码次要是将this对象当中的内存偏移地址为stateOffset的对象拿进去与NEW进行比较,如果等于NEW那就将这个值设置为CANCELLED,这整个操作是原子的(因为可能多个线程同时调用这个函数,因此需要保障操作是原子的),如果操作胜利返回true反之返回false。如果你目前不是很理解也没关系,只需要知道它是将对象this的内存偏移为stateOffset的值替换为CANCELLED就行,如果这个操作胜利返回true,不胜利返回false。 FutureTask回顾咱们首先来回顾一下FutureTask的编程步骤: 写一个类实现Callable接口。 @FunctionalInterfacepublic interface Callable<V> { /** * Computes a result, or throws an exception if unable to do so. * * @return computed result * @throws Exception if unable to compute a result */V call() throws Exception;}复制代码实现接口就实现call即可,可能看到这个函数是有返回值的,而FutureTask返回给咱们的值就是这个函数的返回值。 ...

August 7, 2022 · 5 min · jiezi

关于前端工程化:超详细一次搞定Eslint-Prettier-husky-lintstaged前端代码工作流

咱们应用热门构建工具vite创立我的项目,并抉择react + ts模板。对于Eslint + Prettier + husky + lint-staged配置同样实用于webpack、Vue创立的我的项目,稍有不同的中央下文会提到,请释怀食用。 1、应用vite创立我的项目yarn create vite输出我的项目名并抉择 react + ts success Installed "create-vite@3.0.0" with binaries: - create-vite - cva✔ Project name: … my-app✔ Select a framework: › react✔ Select a variant: › react-tsDone. Now run: cd my-app yarn yarn dev2、装置Eslintyarn add -D eslint3、初始化配置Eslintnpm init @eslint/config或者 npx exlint --init3.1、抉择模式:选查看语法并发现问题 ? How would you like to use ESLint? … To check syntax only❯ To check syntax and find problems To check syntax, find problems, and enforce code style3.2、抉择模块化语法:选JavaScript modules ...

July 18, 2022 · 3 min · jiezi

关于前端工程化:体系课吃透前端工程化大厂级实战项目以战带练MKW无密

download:吃透前端工程化,大厂级实战我的项目以战带练复制下哉ZY:https://www.97yrbl.com/t-1383.html @Componentpublic class RedisUtil { public static RedisUtil util; public RedisUtil(@Autowired JedisPool jedisPool) { this.jedisPool = jedisPool; RedisUtil.util = this; } public static RedisUtil getInstance(){ return util; } @Autowired private JedisPool jedisPool; /** * 向Redis中存值,永恒无效 */ public String set(String key, String value) { Jedis jedis = null; try { jedis = jedisPool.getResource(); return jedis.set(key, value); } catch (Exception e) { return "0"; } finally { jedis.close(); } } /** * 依据传入Key获取指定Value */ public String get(String key) { Jedis jedis = null; String value; try { jedis = jedisPool.getResource(); value = jedis.get(key); } catch (Exception e) { return "0"; } finally { jedis.close(); } return value; } /** * 校验Key值是否存在 */ public Boolean exists(String key) { Jedis jedis = null; try { jedis = jedisPool.getResource(); return jedis.exists(key); } catch (Exception e) { return false; } finally { jedis.close(); } } /** * 删除指定Key-Value */ public Long del(String key) { Jedis jedis = null; try { jedis = jedisPool.getResource(); return jedis.del(key); } catch (Exception e) { return 0L; } finally { jedis.close(); } } /** * 分布式锁 * @param key * @param value * @param time 锁的超时工夫,单位:秒 * * @return 获取锁胜利返回"OK",失败返回null */ public String getDistributedLock(String key,String value,int time){ Jedis jedis = null; String ret = ""; try { jedis = jedisPool.getResource(); ret = jedis.set(key, value, new SetParams().nx().ex(time)); return ret; } catch (Exception e) { return null; } finally { jedis.close(); } } public void pub(){ jedisPool.getResource().publish("test","msg"); } }

April 16, 2022 · 2 min · jiezi

关于前端工程化:吃透前端工程化大厂级实战项目以战带练一起无mi

前端工程化download:https://www.sisuoit.com/2764.... 课成下崽链接:https://www.sisuoit.com/2764.html最晚期写前端代码时,往往一个页面就是一个文件,HTML/CSS/JS全副写在一起,起初晓得应该把结构层、体现层和行为层拆散;再起初随着工程复杂化,产生的诸多问题,比方:如何进行高效的多人合作?如何保障我的项目的可维护性?如何进步我的项目的开发效率?前端工程化思维就在这时利用而生。所谓前端工程化,我认为就是将前端我的项目当成一项系统工程进行剖析、组织和构建从而达到我的项目构造清晰、分工明确、团队配合默契、开发效率进步的目标。 前端模块化模板化是在文件层面上,对代码和资源的拆分。就是将一个大文件拆分成相互依赖的小文件,再进行对立的拼装和加载。那具体什么是模块化呢,举一个简略的例子,咱们要写一个实现A性能的JS代码,这个性能在我的项目其余地位也须要用到,那么咱们就能够把这个性能看成一个模块采纳肯定的形式进行模块化编写,既能实现复用还能够分而治之,同理在写款式的时候,如果咱们须要某种非凡的款式,会在很多中央利用,那么咱们也能够采纳肯定的形式进行CSS的模块化。具体说来,JS模块化计划很多有AMD/CommonJS/UMD/ES6 Module等,CSS模块化开发大多是在less、sass、stylus等预处理器的import/mixin个性反对下实现的。模块化不难理解,重点是要学习相干的技术并且灵活运用。前端组件化组件化是在设计层面上,对于UI的拆分。组件化将页面视为一个容器,页面上各个独立局部例如:头部、导航、焦点图、侧边栏、底部等视为独立组件,不同的页面依据内容的须要,去盛放相干组件即可组成残缺的页面。组件具备独立性,因而组件与组件之间能够自由组合;当不须要某个组件,或者想要替换组件时,能够整个目录删除/替换。 前端自动化“简略反复的工作交给机器来做”,自动化也就是有很多自动化工具(glup、webpack)代替咱们来实现,例如继续集成、自动化构建、自动化部署、自动化测试等等。之后的文章中作具体介绍前端规范化在我的项目布局初期制订的好坏对于前期的开发有肯定影响。包含的标准有:目录构造的制订、编码标准、前后端接口标准、文档标准、组件治理、代码包治理(SVN、Git)、commit提交代码备注形容标准、定期codeReview、视觉图标标准.......总之: 这些概念在软件工程里都是很根底的常识,次要是前端倒退快,需要大,变动灵便,才从简略的前端页面倒退成前端工程化,开发人员还是须要一直的学习,逆水行舟。

March 27, 2022 · 1 min · jiezi

关于前端工程化:前端工程化打造企业通用脚手架

前言转载自搜狐-前端工程化-打造企业通用脚手架随着前端工程化的概念越来越深刻FEer心,前端开发过程的技术选型、代码标准、构建公布等流程的规范化、标准化是须要工具来保驾护航的,而不是每次都对反复工作进行手动复制粘贴。脚手架则可作为工程化的辅助工具,从很大水平上为前端研发提效。 脚手架是什么?那脚手架是什么呢? 在以往工作中,咱们可能须要先做如下操作能力开始编写业务代码: 技术选型初始化我的项目,抉择包管理工具,装置依赖编写根底配置项配置本地服务,启动我的项目开始编码随着Vue/React的衰亡,咱们能够借助官网提供的脚手架vue-cli或create-react-app在命令行中通过抉择或输出来按咱们的要求和爱好疾速生成我的项目。它们能让咱们专一于代码,而不是构建工具。 脚手架能力然而这些脚手架是针对于具体语言(Vue/React)的,而在咱们理论工作中不同BU针对不同端(PC、Wap、小程序...)所采纳的技术栈也可能不同,往往特定端采纳的技术栈在肯定水平上都能够复用的到其余相似我的项目中。咱们更冀望能在命令行通过几个命令和抉择、输出构建出不同端不同技术栈的我的项目。 上述只是新建我的项目的例子,前端开发过程中不止于此,个别有如下场景: 创立我的项目+集成通用代码。我的项目模板中蕴含大量通用代码,比方通用工具办法、通用款式、通用申请库解决HTTP申请、外部组件库、埋点监控...Git操作。个别须要手动在Gitlab中创立仓库、解决代码抵触、近程代码同步、创立版本、公布打Tag...等操作。CICD。业务代码编写实现后,还须要对其进行构建打包、上传服务器、域名绑定、辨别测试正式环境、反对回滚...等继续集成、继续部署操作。为什么不必自动化构建工具个别状况下,咱们会采纳Jenkins、Gitlab CI、Webhooks等进行自动化构建,为什么还须要脚手架? 因为这些自动化构建工具都是在服务端执行的,在云端就无奈笼罩研发同学本地的性能,比方上述创立我的项目、本地Git操作等;并且这些自动化工具定制过程须要开发插件,前端同学对语言和实现须要肯定学习和工夫老本,前端同学也更冀望只应用JavaScript就能实现这些性能。 脚手架外围价值综上,前端脚手架存在意义重大。脚手架的外围指标是晋升前端研发整个流程的效力。 自动化。防止我的项目反复代码拷贝删改的场景;将我的项目周期内的Git操作自动化。标准化。疾速依据模板创立我的项目;提供CICD能力。数据化。通过对脚手架本身埋点统计,将耗时量化,造成直观比照。往往各个公司对于自动化和标准化的局部性能Git操作、CICD都有实现一套欠缺的相似于代码公布管理系统,帮忙咱们在Gitlab上治理我的项目,并提供继续集成、继续部署的能力。更有甚者,针对小程序的我的项目也会对其进行代码公布治理,将其规范化。 咱们可能就只须要思考 创立我的项目+集成通用代码常见痛点的解决方案(疾速生成页面并配置路由...)配置(eslint、tsconfig、prettier...)提效工具(拷贝各种文件)插件(解决webpack构建流程中的某个问题...)...上面则介绍咱们在公司外部基于这些场景所做的尝试。 应用脚手架首先在终端通过focus create projectName命令新建一个我的项目。其中focus示意主命令,create示意command,projectName示意command的param。而后依据终端交互去抉择和输出最终生成我的项目。 咱们为各个BU、各个端、各个技术栈提供不同模板我的项目,于此同时,每个同学都能将小组内的我的项目积淀并提炼成一个模板我的项目,并按肯定标准集成到脚手架中,反哺整个BU。 @focus/cli架构如下架构图,采纳Lerna做我的项目的管理工具,目前babel、vue-cli、create-react-app大型项目均采纳Lerna进行治理。它的劣势在于: 大幅缩小反复操作。多个Package时的本地link、单元测试、代码提交、代码公布,能够通过Lerna一键操作。晋升操作的标准化。多个Package时的公布版本和相互依赖能够通过Lerna放弃一致性。 在@focus/cli脚手架中,依据性能进行拆分: @focus/cli寄存脚手架次要性能 focus create projectName拉取模板我的项目focus add material新建物料,能够是一个package、page、component...粒度可大可小focus cache革除缓存、配置文件信息、长期寄存的模板focus domain拷贝配置文件focus upgrade更新脚手架版本,也有主动询问更新机制@focus/eslint-config-focus-fe寄存组内对立的eslint规定也可通过focus add material新建子Package实现特定性能...依赖项概览一个脚手架外围性能须要依赖以下根底库去做撑持。 chalk:控制台字符款式commander:node.js命令行接口的残缺解决方案fs-extra:加强的根底文件操作库inquirer:实现命令行之间的交互ora:优雅终端Spinner期待动画axios:联合Gitlab API获取仓库列表、Tags...download-git-repo:从Github/Gitlab中拉取仓库代码consolidate :模板引擎整合库。次要应用ejs实现模板字符替换ncp :像cp -r一样拷贝目录、文件metalsmith :可插入的动态网站生成器;例如获取到依据用户自定义的输出或抉择配合ejs渲染变量后的最终内容后,通过它做插入批改。semver :获取库的无效版本号ini :一个用于节点的ini格局解析器和序列化器。次要是对配置做编码和解码。jscodeshift :能够解析文件将代码从AST-to-AST。例如新建一个页面后须要在routes.ts中新建一份路由。采纳Typescript编码,应用babel编译。 除了tsc之外,babel7也能编译typescript代码了,这是两个团队单干一年的后果。然而babel因为单文件编译的特点,做不了和tsc的多文件类型编译一样的成果,有几个个性不反对(次要是 namespace 的跨文件合并、导出非 const 的值),不过影响不大,整体是可用的。babel 做代码编译,还是须要用 tsc 来进行类型查看,独自执行 tsc --noEmit 即可。 援用自为什么说用 babel 编译 typescript 是更好的抉择{ "scripts": { "dev": "npx babel src -d lib -w -x \".ts, .tsx\"", "build": "npx babel src -d lib -x \".ts, .tsx\"", "lint": "eslint src/**/*.ts --ignore-pattern src/types/*", "typeCheck": "tsc --noEmit" }, }在pre-commit中须要先npm run lint && npm run typeCheck再build最初能力提交代码。 ...

January 13, 2022 · 4 min · jiezi

关于前端工程化:记一次在老掉牙的Vue2项目中引入TypeScript和组合式Api的艰辛历程

原由现有的一个我的项目2年前创立的,随着工夫流逝,代码量曾经暴增到了将近上万个文件,然而工程化曾经缓缓到了不可保护的状态,想给他来一次大换血,然而侵入式代码配置太多了……,最终以一种斗争的形式引入了TypeScript、组合式Api、vueuse,晋升了我的项目的工程化标准水平,整个过程让我颇有感概,记录一下。 先配置TypeScript相干的一些库的装置和配置因为webpack的版本还是3.6,尝试数次降级到4、5都因为大量的配置侵入性代码的大量批改工作放弃了,所以就间接找了上面这些库 npm i -D ts-loader@3.5.0 tslint@6.1.3 tslint-loader@3.6.0 fork-ts-checker-webpack-plugin@3.1.1接下来就是改webpack的配置了,批改main.js文件为main.ts,并在文件的第一行增加// @ts-nocheck让TS疏忽查看此文件,在webpack.base.config.js的入口中相应的改为main.ts在webpack.base.config.js的resolve中的extensions中减少.ts和.tsx,alias规定中减少一条'vue$': 'vue/dist/vue.esm.js'在webpack.base.config.js中减少plugins选项增加fork-ts-checker-webpack-plugin,将ts check的工作放到独自的过程中进行,缩小开发服务器启动工夫在 webpack.base.config.js文件的rules中减少两条配置和fork-ts-checker-webpack-plugin的插件配置 { test: /\.ts$/, exclude: /node_modules/, enforce: 'pre', loader: 'tslint-loader'},{ test: /\.tsx?$/, loader: 'ts-loader', exclude: /node_modules/, options: { appendTsSuffixTo: [/\.vue$/], transpileOnly: true // disable type checker - we will use it in fork plugin }},,// ...plugins: [new ForkTsCheckerWebpackPlugin()], // 在独立过程中解决ts-checker,缩短webpack服务冷启动、热更新工夫 https://github.com/TypeStrong/ts-loader#faster-builds根目录中减少tsconfig.json文件补充相应配置,src目录下新增vue-shim.d.ts申明文件 tsconfig.json { "exclude": ["node_modules", "static", "dist"], "compilerOptions": { "strict": true, "module": "esnext", "outDir": "dist", "target": "es5", "allowJs": true, "jsx": "preserve", "resolveJsonModule": true, "downlevelIteration": true, "importHelpers": true, "noImplicitAny": true, "allowSyntheticDefaultImports": true, "moduleResolution": "node", "isolatedModules": false, "experimentalDecorators": true, "emitDecoratorMetadata": true, "lib": ["dom", "es5", "es6", "es7", "dom.iterable", "es2015.promise"], "sourceMap": true, "baseUrl": ".", "paths": { "@/*": ["src/*"], }, "pretty": true }, "include": ["./src/**/*", "typings/**/*.d.ts"]}vue-shim.d.ts ...

January 13, 2022 · 3 min · jiezi

关于前端工程化:Bit-共享代码

Think in ComponentBit是组件驱动架构,基于组件的古代利用开发。在Bit的世界里,所有皆组件。 组件能够组合成其余组件,最终组成一个利用APP,即APP也是组件的一种。 这为咱们开发提供一个新的思路:咱们构建能够整合成不同利用的组件,而不是构建蕴含组件的利用。 Bit帮咱们构建模块化、巩固的、可测试、可复用的代码。 Bit Cloud是组件的云托管服务。它为开发人员和团队提供端到端的解决方案,用于托管、组织、检索、应用、更新和合作解决组件。 Bit劣势以组件架构的思维帮忙咱们构建模块化、巩固的、可测试、可复用的代码。从现有代码构造中拆散组件,无需更改构造,或保护新的我的项目。可更改依赖组件,并创立本人的版本独立治理,无需担心净化其它环境。初始化Bit工作区装置BVM & BitBVM是Bit版本管理工具,雷同NVM // node版本12.22.0以上npm i -g @teambit/bvm执行bvm -h测验是否装置胜利,若揭示bvm命令不可用,须要设置环境变量: # MacOs Bashecho 'export PATH=$HOME/bin:$PATH' >> ~/.bashrc && source ~/.bashrc# zshecho 'export PATH=$HOME/bin:$PATH' >> ~/.zshrc && source ~/.zshrc# windowssetx path "%path%;%LocalAppData%\.bvm"装置最新版bit: bvm install执行bit -h测验是否装置胜利,若揭示bit命令不可用,须要按上述流程设置一下环境变量。 bit new命令初始化工作区实用于新建我的项目$ bit new <env> <project>$ cd <project>$ bit installbit init命令初始化工作区实用于已有我的项目先初始化环境$ cd <project>$ bit init --harmony手动配置开发环境以react环境为例,批改workspace.jsonc文件: "teambit.workspace/variants": { "*": { "teambit.react/react": { } }}装置必要的peer依赖$ bit install react --type peer$ bit install react-dom --type peer初始化Git须要将workspace.jsonc和.bitmap 上传到Git。 ...

November 19, 2021 · 6 min · jiezi

关于前端工程化:后端视野学-Webpack-文武双全

大家好,我是小菜。一个心愿可能成为 吹着牛X谈架构 的男人!如果你也想成为我想成为的人,不然点个关注做个伴,让小菜不再孤独! 本文次要介绍 Webpack 应用 如有须要,能够参考 如有帮忙,不忘 点赞 ❥ 微信公众号已开启,小菜良记,没关注的同学们记得关注哦! 前端认知挺多人对前端开发是存在肯定的误会的,感觉会点 H5 + C3 + JS 就等于会前端开发,但近几年前后端拆散的模式逐步流行起来,就阐明前端早已没有之前那么简略。 站在我这个后端的视角上倒感觉, 前端是个武官,后端是个武将,不能说做到能文能武,但起码求武的同时不能不识一丁,退一两步来说,以后端实习妹子遇到 Bug 束手无措的时候,你这伪境前端若能出手相助~那在她人眼中你就是一位 '架着七彩祥云而来的盖世英雄~' 如果说你会 Bootstrap 加上 Layui,那么就说你会前端,难免会被人拍死在沙滩上~ 实际上的前端开发是由以下几个模块组成: 模块化(js 的模块化,css 的模块化,资源的模块化)组件化(复用现有的 UI 构造,款式,行为)规范化(目录构造的划分、编码规范化、接口规范化、文档规范化、Git分支治理)自动化(自动化构建、主动部署、自动化测试) 与后端一模一样,该有的模块都有。 说到工程化,在后端开发中存在支流的解决方案有 Maven 工程 和 Gradle 工程。前端工程化解决方案也有 webpack 和 vite。那么就进入咱们明天的正题,走进 Webpack Webpack一、概念认知实质上,webpack 是一个用于古代 JavaScript 应用程序的动态模块打包工具。当 webpack 解决应用程序时,它会在外部从一个或多个入口点构建一个依赖图,而后将你的我的项目中所需的每一个模块组合成一个或多个 bundles,它们均为动态资源,用来展现你的内容。 以上内容摘于官网,官里官气的。上面咱们简略概括一下概念总结:webpack 是前端我的项目工程化的具体解决方案性能总结: 提供了敌对的前端模块化开发的反对提供了代码压缩混同、解决浏览器兼容Js、以及性能优化等弱小性能长处总结:进步了前端开发效率和我的项目的可维护性二、根本应用实际出真知!咱们间接应用来增强意识。 首先咱们须要创立一个空白目录,而后在空白目录中执行 npm init -y 来初始化包治理配置文件 package.json 能够简略了解为这个 package.json 就相当于 maven 工程中的 pom.xml 文件在 Maven 工程中咱们通常上都是把源代码放在 src 目录底下,该 webpack 工程相似,因而咱们下一步便是在该目录下创立 src 目录,继而创立两个文件 index.html (首页) 和 index.js (脚本文件) ...

August 30, 2021 · 4 min · jiezi

关于前端工程化:全网独发立马就能复制粘贴的轻量级前端工程化解决方案

对于gh-frameworkgh-framework旨在解决vue2环境下(思维可用于任何框架我的项目,不局限于vue2)的前端工程化问题,它将封装vue我的项目中罕用的工具库和配置文件并将其可移植化,例如axios、constants(常量)、directives(指令)、services(数据申请层)、config(配置文件)、mixin(全局混入)、utils(工具集)、context(上下文)。本计划自己已在5+我的项目上利用,包含一个大型前端我的项目。 github地址(示例代码):https://github.com/cong1223/gh-framework 个性高度封装:高度封装我的项目常用工具和配置,不写反复代码。疾速移植化:封装一次,其余类型我的项目可复制粘贴,疾速聚拢反复代码,依据业务需要小局部批改即可,如果后端返回数据格式统一,那么services都不须要批改即可利用。不具备破坏性:新我的项目如果想尝试这套解决方案,那么能够移植次计划,并且对你原先的我的项目不具备破坏性,能够同时兼容。疾速开发体验:一次封装,永恒劳碌,辞别繁琐的导入/导出,this.万物。适用人群对前端工程化具备强烈学习趣味的初中级前端程序员;前端我的项目中表演captain角色的程序员;疾速交付型守业程序员;兼职接单程序员;我的项目构造介绍疏忽了vue我的项目根本我的项目构造文件 |-- node_mudules|-- public|-- src |-- assets // 动态资源文件夹 |-- config // 配置文件文件夹 |-- const // 常量配置文件夹 |-- framework // gh-framework 文件夹 |-- directives // 全局指令文件夹 |-- mixin // 全局mixin混入文件夹 |-- plugins // framework 外围工具集的配置入口 |-- utils // 全局工具集文件夹 |-- ui // 全局通用ui组件文件夹 |-- config.js // 文件名映射配置文件(重要) |-- index.js // 导出为vue能装置的framework插件(封装为install函数) |-- services // 数据申请层文件夹|-- .browserslistrc|-- .eslintrc.js|-- .gitignore|-- babel.config.js|-- package.json|-- README.md|-- yarn.lockframeworkframework文件夹就是gh-framework的核心思想所在,高度聚拢我的项目公共代码,应用变得能够疾速移植化,一个我的项目做完了,能够立马复制framework文件夹到另外一个新的我的项目,惟一不同的就是业务代码局部。此文件夹能够依据本人的业务需要持续扩大其余通用逻辑,封装办法参考directives、utils等。 directives全局指令封装集文件夹,将我的项目中罕用指令对立治理,全局装置。 文件夹我的项目构造如下: |-- directives |-- debounce.js // 避免按钮在短时间内被屡次点击,应用防抖函数限度规定工夫内只能点击一次 |-- loadmore.js // element-ui下拉框下拉更多 |-- draggable.js // 实现一个拖拽指令,可在页面可视区域任意拖拽元素 |-- copy.js // 复制粘贴指令 |-- emoji.js // 不能输出表情和特殊字符,只能输出数字或字母等 |-- lazyload.js // 实现一个图片懒加载指令,只加载浏览器可见区域的图片 |-- longpress.js // 实现长按,用户须要按下并按住按钮几秒钟,触发相应的事件 |-- permission.js // 权限指令,对须要权限判断的 Dom 进行显示暗藏 |-- watermark.js // 给整个页面增加背景水印 |-- // 更多其余指令... |-- index.js // 对立进口以上更多通用指令请在示例我的项目中查看获取debounce.js ...

July 18, 2021 · 14 min · jiezi

关于前端工程化:翻译篇-ES-模块预加载和完整性

翻译篇 - ES 模块预加载和完整性<!-- TOC --> 翻译篇 - ES 模块预加载和完整性 生产环境中模块的优化模块预加载模块预加载例子:Polyfilling 模块预加载完整性限度号召一起口头JSPM Generator - 模块预加载生成器参考社交信息 / Social Links:<!-- /TOC --> 生产环境中模块的优化当在生产环境中应用ES模块时,目前有两种次要的性能优化要利用-代码拆分和预加载。 代码拆分优化可用于打包器(如 esbuild 或 RollupJS)中的原生 ES 模块。代码拆分确保对于任何两个总是被一起加载的模块,它们将始终被内联到同一个模块文件中,作为网络优化的块模块(或者甚至在可能的状况下,会被内联到入口模块自身中)。 而后预加载解决了模块依赖图中模块按援用程序加载的问题 - 模块仅在动态导入图中的每个模块都加载后才执行,模块仅在其父模块加载后才加载。参考:实操探索之ESM引入顶级await前后模块的执行程序 模块预加载ES 模块的预加载在浏览器中如下写法: <link rel="modulepreload" href="..."/> 当它首次在 Chrome 中公布时,Google Developers 2017 更新中有一篇对于它的精彩文章。 倡议尽可能为所有深度依赖项注入 modulepreload 标签,以便齐全打消模块加载的提早老本,这同时也是是动态预加载的次要益处。【应用 modulepreload 的另一个次要益处是,它是目前应用“integrity”属性反对所有加载模块齐全完整性的惟一机制。】(不太明确,等我查找材料) 比方app.js加载dependency.js加载library.js,咱们能够这样写: <link rel="modulepreload" href="/src/app.js" integrity="sha384-Oe38ELlp8iio2hRyQiz2P4Drqc+ztA7jb7lONj7H3Cq+W88bloPxoZzuk6bHBHZv"/><link rel="modulepreload" href="/src/dependency.js" integrity="sha384-kjKb2aJJUT956WSU7Z0EF9DZyHy9gdvPOvIWbcEGATXKYxJfkEVOcuP1q20GT2LO"/><link rel="modulepreload" href="/src/library.js" integrity="sha384-Fwh0dF5ROSVrdd/vJOvq0pT4R6RLZOOvf6k+ijkAyUtwCP7B0E3qHy8wbot/ivfO"/><script type="module" src="/src/app.js"></script>因为预加载导致 app.js、dependency.js 和 library.js 当初立刻并行加载,因而模块提早加载被打消,【并且通过所有脚本的完整性,咱们能够齐全爱护模块执行环境。】(不太明确,等我查找材料) 模块预加载例子:代码: index.html: <!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <title>Document</title> <!-- <link rel="modulepreload" href="./index.js"/> <link rel="modulepreload" href="./init.js"/> <link rel="modulepreload" href="./a.js"/> <link rel="modulepreload" href="./b.js"/> --></head><body> <script src="./index.js" type="module"></script></body></html>index.js: import './init.js'import {a, Person, obj, b} from './a.js'console.log(a, Person, obj, b)console.log('index')init.js: ...

July 11, 2021 · 2 min · jiezi

关于前端工程化:git钩子与自动化部署

webhook定义Webhooks allow you to build or set up integrations, such as GitHub Apps , which subscribe to certain events on GitHub.com. When one of those events is triggered, we'll send a HTTP POST payload to the webhook's configured URL. Webhooks can be used to update an external issue tracker, trigger CI builds, update a backup mirror, or even deploy to your production server. You're only limited by your imagination.留神要害语句,当咱们触发一个事件(比方git push)的时候,github会给我配置的url发一个post申请,并携带一些参数 主动部署过程这次咱们玩的正规一点 ...

January 11, 2021 · 2 min · jiezi

关于前端工程化:git钩子与自动化部署

webhook定义Webhooks allow you to build or set up integrations, such as GitHub Apps , which subscribe to certain events on GitHub.com. When one of those events is triggered, we'll send a HTTP POST payload to the webhook's configured URL. Webhooks can be used to update an external issue tracker, trigger CI builds, update a backup mirror, or even deploy to your production server. You're only limited by your imagination.留神要害语句,当咱们触发一个事件(比方git push)的时候,github会给我配置的url发一个post申请,并携带一些参数 主动部署过程这次咱们玩的正规一点 ...

January 11, 2021 · 2 min · jiezi

关于前端工程化:构建高质量的前端工程

在过来,与大多数工程师一样,我认为前端代码的设计程度大多与工程师的能力有间接关系。但随着接手了几个多人合作的大型前端我的项目,我开始意识到,这种认知对短生命周期的小型我的项目可能实用,但对真正的大型项目,仅靠晋升工程师品质有时并不能间接晋升代码的品质。 本文将联合本人的一些理论教训,来论述本人的一个观点:构建大型高质量前端工程,正当的代码束缚与正确的团队运行机制可能更为重要。 什么是高质量的工程代码?高质量的工程代码,并不等价于性能最优,技术最新,复用性最强的技术选型。回顾几年前的前端畛域:JQuery 时代,尽管要手动操作 DOM,但其实在那时, Google Closure 和 Ext.js 团队就曾经提供了残缺的组件化概念,甚至 Ext.js 还提供了组件冒泡这样的翻新事件机制。那时用 Zepto 保护的代码,编码速度甚至比当初写一些 React 我的项目还要快。不同的技术只是工具,怎么用工具,能把工具用到什么水平,最终取决于开发者本身,所以高质量的工程代码,更多应该从业务和工程的角度思考问题,而非技术选型。 举个例子,当整个公司都在应用 React 开发时,尽管咱们晓得 Vue 应用可能会更简略便捷,但咱们肯定不会去用,因为这个时候,尽管看起来写代码更简略了,但其他人在 React 方向积淀的教训,你无奈复用,额定还须要整个团队去学习一套全新的技术,这样的工程设计,在这个背景下,显然是不合理的。 Thenewstack 做过 2019 年的开发者数据统计,开发者 32% 的工夫用于业务开发,19% 的工夫在保护代码,也就是工程师真正能投入到研发中的工夫也只有工作工夫的一半。对于开发者来说,这个时候通过正当的代码设计,晋升代码的可扩展性,可维护性,升高开发和保护代码的工夫,才是最强的诉求。 所以,高质量的工程代码应该是联合业务与团队状况,真正可能晋升研发效率,升高我的项目保护老本的代码。 谁决定了工程代码的品质?这里能够用木桶实践来类比:木桶中的水位,不取决于最高的木板,而取决于最低的木板。同理,前端工程的品质,不取决于团队的均匀能力,而取决于团队教训较少的技术同学的能力。在工作压力比拟大的状况下,这些同学因为经验不足,短期又要实现需要,所以很多时候,并没有思考过工程上的问题,而是间接面向实现性能编程,基本上咱们当初面对的难以保护的代码,都是在这种条件下产生的。 咱们当然能够寄希望于教训较少的同学通过一直的成长来晋升我的项目的工程质量,但实际下来,这并不可行。起因在于,工程能力的积攒须要大量的编码教训,短少实践经验的问题并不是短期就可能迅速解决的,任何好的工程师都是在一直犯错学习的过程中成长起来的。同时,工程开发过程中很可能会遭逢人员变动,一个团队的成员不肯能永远全部都是能力很强的。 那么咱们就须要换一个策略来保障咱们的代码品质,咱们能够换个角度思考:是否能够通过一些规定,流程,编码上的束缚,让编码能力不同的工程师,尽量写出品质绝对较高的一致性代码。 通过束缚晋升工程质量束缚让事件变得更简略工作没有束缚,工作中咱们就难以造成共识,也无奈判断工作做的好与坏。写代码也是一样的,没有束缚,那么咱们也无奈判断代码是否正当。在风行的库和框架中,其实到处都是束缚的影子,这里拿 Redux 和 React 的设计来举例: Redux 给出了繁多数据源,State 只读,应用纯函数来执行批改这三个根本准则,同时要求通过 Action 的形式触发Reducer 的执行,这是 Redux 的束缚;React 也给出了单向数据流这样的束缚概念。 框架之所以是可能复用,可能失去推广,就是因为它们进行了封装,仅仅提供无限的束缚能力供大家应用,这样大家能力造成统一的理念,编写相互可能读得懂的代码。了解了这一点,咱们再来看业务工程的代码,实际上要进步开发效率和扩展性,无非也是要提供正当的束缚。 工程代码的束缚,更多带有肯定的工程属性,如: 规定雷同的申请地址只容许在 API 层呈现一次(我的项目接口数目多,可缩小代码冗余)不应用超过 100 行以上的 Hook 代码(强化逻辑拆分,防止适度简单的逻辑)在复用性和可维护性上做抉择时,优先选择可维护性(防止谬误封装,封装代码中耦合大量逻辑判断)业务代码正文覆盖率必须超过 10%(晋升代码可读性,不便自动化生成文档)我的项目中跨组件通信必须通过 Redux (升高组件传值代码的团队了解老本)雷同性能的 npm 包不容许装置多个(防止无用依赖装置,造成保护成本增加)这些业务的束缚,并不等同于 Eslint,不同的业务对代码的要求有可能千差万别,所以业务上的束缚,须要研发人员充沛的沟通交流,碰撞探讨,以及坚决执行。不同团队的同学,可能探讨出的后果齐全不同,但束缚的论断是什么自身不重要,重要的是造成统一的开发共识。 通过机制实现束缚的落地束缚自身并不难制订,对于工程侧的设计,工程师通过探讨比拟容易造成博奕后的论断。但机制的落地是绝对艰难的一环。这里分享几个可执行的保障机制: CodeReview(每次CR,除了对业务进行逻辑剖析,也须要将是否遵循束缚作为审核的一环)通过工具主动生成局部代码(比方应用脚手架生成工程代码中的某个模块,相似 AngularCLI 中 ng g component header 这样的指令,就能够帮你束缚组件创立的代码构造)配置化生成代码(通过配置,生成逻辑或者表单代码,建设配置项规范)零代码 / PaaS 平台(通过平台生成代码,间接将用户与编码隔离,由平台保障生成代码的品质)负责人机制(束缚落地间接与绩效相关联,成为跟进明确指标)积淀文档(通过文档,积淀束缚机制)通过这样的一些机制,保障束缚无效的落地,那么咱们就能够抹平团队成员技术能力的差别,造成一致性的编码格调。尽管这种束缚下的代码并不一定是最优雅的代码,但至多工程质量不会差。所以这里我认为,束缚实际上帮忙咱们保障的是工程质量的上限,那么接着咱们来谈如何通过技术创新,晋升工程质量的下限。 ...

December 17, 2020 · 1 min · jiezi

关于前端工程化:前端工程化上21

技术是为了解决问题存在的以前那种写demo套模板,曾经远远不能满足咱们当初开发要求了,所以就有了前端工程化的概念。 既然说了技术是为了解决问题存在的,前端工程化也不例外。问题: 想应用es6+新个性,然而浏览器不兼容想要应用less,sass,PostCss加强css个性,然而运行环境不能间接反对想要模块化的形式进步我的项目可维护性,然而运行环境不能间接反对部署前须要手动压缩代码及资源文件部署过程须要手动上传代码到服务器多人协同开发,无奈硬性对立大家代码格调,从仓库pull代码品质无奈保障局部性能开发时须要期待后端服务接口提前完成次要解决的问题: 工程化的体现:所有以提高效率,降低成本,质量保证为目标的伎俩都属于工程化。所有反复的工作都应该被工程化。 创立我的项目: 创立我的项目构造创立特定类型文件编码: 格式化代码校验代码格调编译/构建/打包预览/测试: web Server/MockLive Reloading/HMRSource Map提交: Git HooksLint-staged继续集成部署: CI/CD主动公布工程化不是某个工具

November 9, 2020 · 1 min · jiezi

关于前端工程化:如何提升前端基建的效能价值

写在后面上一篇如何掂量工具平台的效力价值?推导出了一种度量模型,通过具体的数据指标来掂量效力价值,让外部工具/平台的价值也能看得见、说得清 那么,对于正在做或者将要做的工具平台,如何进一步晋升其效力价值呢? 一.效力价值有哪些影响因素?首先,工具的要害指标是解决理论问题: 工具总是为解决问题而生的选定指标问题之后,接着通过工具化、平台化等主动/半自动的伎俩来尝试解决,并通过效率和体验两方面的晋升体现出解决方案的效力价值: 效力价值 = 效率价值 * 体验因子进一步细化: 工具效率 = 问题规模 / 操作工夫工具效率 = (不必该工具解决所需的)工夫老本 / (用该工具解决所需的)工夫老本工具体验 = 易用水平 * 稳固水平因而,工具的效力价值取决于 4 个因素: 问题规模操作工夫易用性稳定性晋升工具效力就是想方法增大分子、减小分母,即晋升问题规模、易用性、稳定性,升高操作工夫 二.如何晋升问题规模?对于选定的指标问题,其规模通常是固定的,所以关键在于如何抉择指标价值最高的问题: 问题的指标价值 = 指标用户量 * 需要频率 * 单次的价值少数状况下,咱们偏向于抉择指标用户量更大的问题,因为解决一个普遍存在的问题要比解决只有小局部用户才会遇到的非凡问题更有意义 然而,需要频率与单价对指标价值的影响却不那么不言而喻: 其中: 首选高频高价:十分难得的需要,如果有,优先满足不做低频高价:此类需要不值得做高频高价、低频高价并重:大多数需要都是这两类,抉择也都集中在这里在高频高价与低频高价之间,产品经理的个别策略是: 高频抓用户,低频做利润也就是说,后期先通过满足高频高价的需要取得大量用户,中后期再将低频高价的需要思考进来: 先利用高频高价的需要抓用户,因为高频场景和用户互动的机会多,而高价的轻决策场景能够升高用户进入门槛,容易拉新、引流;再用低频高价的需要做利润,因为单价高了,能够切分的蛋糕才大。之所以采取这样的先后秩序,是因为必须有海量用户做根底,低频需要的总量才足够大。三.如何升高操作工夫?当然,如果有显著的待优化项,应该尽快去做,先把工具本身的效率晋升到相当高的水准,缩小用户期待工具运行实现的工夫 但如果工具自身在耗时上曾经没有太大的优化空间,此时就须要将眼光从部分的工具中移出来,放眼全局思考整体优化: 面向过程的视角:流程上,是否缩小一些中间环节,简化工作流面向对象的视角:模式上,是否缩小参加其中的相干角色,缩小人与工具、工具与工具、工具与人之间的交互,缩小一些两头产物流程上,甚至合作模式上的改革通常有机会颠覆先前解决问题的要害门路,绕过既有工具的效率瓶颈,从而大幅升高操作工夫 四.如何晋升易用性?工具型产品的第一要义是用户会用,让用户至多会用,能力体现产品的价值易用性要求产品性能尽可能地合乎用户心智(至多要保障外围性能的易用性),简化交互,升高用户上手应用的学习老本: 从用户心智向产品性能做映射,极致的易用是合乎直觉,上手即用那么,首先要明确用户心智,做法非常简单: 通知用户,这个工具能给你解决什么具体问题。接着(在产品性能不那么合乎直觉的阶段)先教会用户怎么用,性能疏导、老手教程/视频、帮忙文档等都是不错的办法,旨在晋升易用性,让用户先用起来。同时依据用户实在反馈一直优化应用体验,放大产品性能与用户心智之间的差距,使之最终合乎直觉: 心智累赘小(学习成本低)交互敌对UI 好看外围性能流程顺畅除了让产品性能向用户心智聚拢外,还有一种非常规思路是造就用户心智(即扭转用户直觉,使之合乎产品性能),多呈现在颠覆式翻新的场景,必须扭转用户积重难返的直觉能力真正提高效率 五.如何晋升稳定性?从用户心智向产品性能做映射,极致的稳固是齐全信赖,从不狐疑工具会出问题与易用性相比,稳定性是主观而明确的,单从技术角度就能在很大水平上确保稳定性,例如: 升高 crash 率:继续关注 top 解体,及时修复影响范畴较大的缩小 bug 数:继续察看 bug 增长趋势,疾速迭代修复,收敛功能性问题缩小操作失败次数:记录失败操作,剖析改善常见误操作,同时反向丰盛性能其中,值得注意是记录失败操作,以搜寻性能为例,失败操作包含: 搜寻服务出错搜寻无后果搜寻后果与预期不符(后果没有帮忙)从技术上看,后两类并不属于操作失败,但同样值得关注,因为无后果的搜寻通常意味着语义化/含糊搜寻性能不够欠缺,或者相干内容有缺失,这些信息对于丰盛产品性能很有帮忙。同理,不合乎用户预期的搜寻后果也是一种有价值的负反馈,有助于发现问题,改善用户应用体验 六.如何晋升用户量?当工具的效率和体验都达标后,最要害的问题是如何晋升用户量,放大工具的价值 与其它产品相比,工具型产品的难点在于: 可替代性强用户不晓得(有工具能够用)用户粘性差,容易散失强的不可替代性是决定性因素,作为惟一选项天然不用思考用户量的问题,例如小程序开发者工具 如果不具备强的不可替代性,就要通过其它伎俩来减少用户的替换老本,罕用的策略有场景化经营、社区经营、内容经营等 场景化经营 将工具与应用场景严密关联起来,造就用户的应用习惯: 做工具型产品肯定要时刻诘问用户在什么样的场景下会想到关上你的产品,这个具体场景就是所有经营的根底围绕一个外围场景,充沛满足要害需要,成为该场景下的最优解决方案,从而解决用户不晓得的问题 另一方面,场景化的舒适提醒有助于晋升产品的温度,让用户感触到兽性关心,而不只是凉飕飕的工具 社区经营增强产品与用户,以及用户与用户的分割,建设社区是进步用户粘性的无效伎俩,例如: ...

October 14, 2020 · 1 min · jiezi

关于前端工程化:前端工程化中的自动化部署

前言       在前端工程化中,前端开发人员终于在一直的进步本人的位置,再也不是简略的切图仔了。当然,随之而来的就是咱们的工作内容变得越来越多,变得越来越繁琐。不仅要一直的学习新的前端技术内容,而且还要独立保护前端的部署工作。因而,如何能疾速的进行工程化内容的部署,就是一件十分有价值的事件。疾速部署       对于前端的部署来说,其实次要就是将编译后的工程化我的项目(以vue来说就是将npm run build的dist文件夹)间接部署到对应的服务器的指定目录下即可,其余的内容咱们临时不在此处做过多的解说。        因而,目前个别会应用SFTP工具(如:FileZilla:https://www.filezilla.cn/,Cyberduck:https://cyberduck.io/)来上传构建好的前端代码到服务器。这种形式尽管也是比拟快的,然而每次要本人进行打包编译=》压缩=》关上对应的工具=》找到对应的门路,手动的将相干的文件上传到对应的服务中。并且为了部署到不同的环境中(个别我的项目开发包含:dev开发环境、test测试环境、uat测试环境、pro测试环境等)还要反复的进行屡次的重复性操作。这种形式不仅繁琐,而且还升高了咱们程序员的身份。所以咱们须要用自动化部署来代替手动 ,这样一方面能够进步部署效率,另一方面开发人员能够少做一些他们认为的无用功。 筹备工作       话不多说,说干就干。首先,咱们先梳理一下个别的部署流程,次要是先将本地的工程化代码进行打包编译=>通过ssh登录到服务器=>将本地编译的代码上传到服务器指定的门路中,具体流程如下:       因而,通过代码实现自动化部署脚本,次要须要实现如下,上面以vue我的项目来为工程项目来具体解说: 实现本地代码编译;可间接配置npm run build即可进行相干的打包实现本地编译的代码压缩3.通过ssh连贯近程服务器 查看对应的近程部署门路是否存在,不存在须要创立上传本地的压缩包解压上传的压缩包删除上传的压缩包敞开ssh连贯删除本地的压缩包具体实现为了在可能实现以上的几个步骤,咱们须要引入一些依赖包,以此来进行相干步骤的操作和日志的打印输出chalk:格式化输入日志的插件,能够通过配置不同的色彩来显示不同的日志打印。https://www.npmjs.com/package/chalkchild_process:nodejs的一个子过程模块,能够用来创立一个子过程,并执行一些工作,能够间接在js外面调用shell命令。http://nodejs.cn/api/child_process.htmljszip:一个轻量级的zip压缩库,用于压缩编译后的脚本。https://stuk.github.io/jszip/ssh2:用于通过ssh来连贯近程服务器的插件。https://github.com/mscdex/ssh2大抵流程如下: 根本配置       为了可能将部署相干的内容,如服务器信息,部署的门路等内容进行对立的治理,以便进行将来的可视化配置。因而,在我的项目中创立一个独立的文件夹,对立治理部署相干的文件,并且建设一个config.js和ssh.js文件,别离用于配置相干的部署信息和具体的部署脚本。其中,config.js中的配置具体如下: const config = { // 开发环境 dev: { host: '', username: 'root', password: '', catalog: '', // 前端文件压缩目录 port: 22, // 服务器ssh连贯端口号 privateKey: null // 私钥,私钥与明码二选一 }, // 测试环境 test: { host: '', // 服务器ip地址或域名 username: 'root', // ssh登录用户 password: '', // 明码 catalog: '', // 前端文件压缩目录 port: 22, // 服务器ssh连贯端口号 privateKey: null // 私钥,私钥与明码二选一 }, // 线上环境 pro: { host: '', // 服务器ip地址或域名 username: 'root', // ssh登录用户 password: '', // 明码,请勿将此明码上传至git服务器 catalog: '', // 前端文件压缩目录 port: 22, // 服务器ssh连贯端口号 privateKey: null // 私钥,私钥与明码二选一 }}module.exports = { // publishEnv: pro, publishEnv: config.pro, // 公布环境 buildDist: 'dist', // 前端文件打包之后的目录,默认dist buildCommand: 'npm run build', // 打包前端文件的命令 readyTimeout: 20000, // ssh连贯超时工夫 deleteFile: true, // 是否删除线上上传的dist压缩包 isNeedBuild: true // s是否须要打包}压缩打包内容       压缩打包的内容,应用JSZIP插件,初始化一个const zip = new JSZIP(),而后在依照对应的语法进行具体的实现,其实现过程次要是通过的先递归读取相干的文件,而后退出到zip对象中,最初通过插件进行具体的压缩,最初写入到磁盘中。具体代码如下:递归读取打包文件 ...

October 5, 2020 · 9 min · jiezi

关于前端工程化:完全-Serverless-文档型动态站点强行变身超薄-SPA

道歉,用 Serverless 做这个题目次要是借势炒作。本文内容跟 Serverless 其实没半毛钱关系。 然而2X作者为啥要起这个名字博眼球呢? 家喻户晓,Serverless 的概念并不像字面意义上那样的 “无服务”,而是将中心化的服务端利用打散成为一个个函数式的服务,节约了前端编码到产品上线两头服务部署的操作老本。实质上是一种 云计算执行模型(Cloud Computing Execution Model)。 聪慧的童鞋应该看完题目和开篇第一张图就猜到了我药里到底卖的什么葫芦 —— 本文内容其实是对于 SSG(Static Site Generation,动态站点生成) 的一个解决方案。介绍一下鄙人最近是如何将一个 Mongo + Egg + React + Node 架构的动静站点(外部非开源版本图表库官网)降(mo)级(gai)为纯动态 SPA (开源图表库 Cloud Charts 的官网站点)并部署在 github pages 上的(注:思考国内拜访问题,目前托管在 gitee 和 netlify)。 为了凸显计划的独特之处,顺便为开源我的项目引引流,未免要先阐明一下我所遇到的需要场景的复杂度和非典型性: 1. 需要介绍1.1 背景略微介绍一下咱们目前正在折(guan)腾(shui)的一个 Github 开源我的项目:阿里云 Cloud Charts —— 又一个开源的信息可视化图表库。 家喻户晓,这绝壁又双叒叕是一群无良技工在反复造轮子!(这里是一段辛辣的讥刺)。 一段不正经的补充阐明: Cloud Charts 的前身是一套长期服务、利用于阿里云混合云场景的数据可视化解决方案(非开源版本),在日常工作中切实不便了很多不相熟前端工作的研发童鞋进行业务实际咱们推出开源版本的其中一个目标,在于寻找更多适宜积淀的业务场景和更多气味相投的优良的小伙伴 -(^_^)/ 没错~就是你!1.2 需要在保护 Cloud Charts 开源我的项目的同时,咱们也须要将其内网版本的文档、示例、教程等材料同步搬运到公网,以不便公网用户对我的项目的理解和应用。 而对于 Cloud Charts 的开源版本(TXD Widgets),咱们曾经实现了一套用于托管相干文档和数据的官网站点(团体内网)。这个外部站点是基于 MongoDB + Egg.js + React.js 模式开发和保护的,最重要的一个特点是依附一个后盾管理系统,不便开发图表库的童鞋可能及时保护和公布图表库我的项目的性能个性和对应的教程、示例、API文档等内容。 ...

September 29, 2020 · 2 min · jiezi

关于前端工程化:快速了解前端开发HTML的正确姿势

摘要:web前端开发(也称为客户端开发)次要是通过html,CSS和JavaScript等前端技术,实现网站在客服端的正确显示及交互性能。一、web规范介绍web规范: w3c:万维网联盟组织,用来制订web规范的机构(组织)web规范:制作网页遵循的标准web标准规范的分类:构造规范、体现规范、行为规范。构造:html。示意:CSS。行为:JavaScript。总结阐明: 构造规范:相当于人的骨架。html就是用来制作网页的。体现规范: 相当于人的衣服。CSS就是对网页进行丑化的。行为规范: 相当于人的动作。JS就是让网页动起来,具备生命力的如果大家还不明确,请看下图 二、浏览器介绍浏览器是网页运行的平台,罕用的浏览器有IE、火狐(Firefox)、谷歌(Chrome)、猎豹浏览器、Safari和Opera等 浏览器内核: PS:「浏览器内核」也就是浏览器所采纳的「渲染引擎」,渲染引擎决定了浏览器如何显示网页的内容以及页面的格局信息。 总结:渲染引擎是兼容性问题呈现的根本原因。 三、开发工具介绍Sublime Text的应用 [Sublime Text应用技巧]Visual Studio Code编辑器WebStormPyCharm.....四、html介绍1、html概念 html全称HyperText Markup Language,翻译为超文本标记语言,它不是一种编程语言,是一种描述性的标记语言,用于形容超文本内容的显示方式。比方字体、色彩、大小等。 超文本:音频,视频,图片称为超文本。标记 :<英文单词或者字母>称为标记,一个HTML页面都是由各种标记组成。作用:HTML是负责形容文档语义的语言。 留神:HTML语言不是一个编程语言(有编译过程),而是一个标记语言(没有编译过程),HTML页面间接由浏览器解析执行。 HTML只是负责形容文档语义的语言,在html中,除了语义,其余什么都没有。 html是一个纯本文文件(就是用txt文件改名而成),用一些标签来形容文字的语义,这些标签在浏览器外面是看不到的,所以称为“超文本”,所以就是“超文本标记语言”了。 So,接下来,咱们必定要学习一堆html中的标签对儿,这些标签对儿可能给文本不同的语义。 2、html的网络术语 网页 :由各种标记组成的一个页面就叫网页。主页(首页) : 一个网站的起始页面或者导航页面。标记: <p>称为开始标记 ,</p>称为完结标记,也叫标签。每个标签都规定好了非凡的含意。元素:<p>内容</p>称为元素.属性:给每一个标签所做的辅助信息。五、HTML的标准HTML是一个弱势语言辨别大小写页面的后缀名是html或者htm(有一些零碎不反对后缀名长度超过3个字符,比方dos零碎)HTML的构造:申明局部:次要作用是用来通知浏览器这个页面应用的是哪个规范。是HTML5规范。head局部:将页面的一些额定信息通知服务器。不会显示在页面上。body局部:咱们所写的须要显示进去的代码必须放在此标签內。1、编写HTML的标准 (1)所有标记元素都要正确的嵌套,不能穿插嵌套。正确写法举例:<h1><font></font></h1> (2)所有的标记都必须小写。 (3)所有的标记都必须敞开。 双边标记:<span></span>单边标记: 转成 转成 ,还有<img src=“URL” />(4)所有的属性值必须加引号。<h1 id="head"></h1> (5)所有的属性必须有值。<input type="radio" checked="checked" /> 2、HTML的根本语法特色 1)、HTML对换行不敏感,对tab不敏感 HTML只在乎标签的嵌套构造,嵌套的关系。谁嵌套了谁,谁被谁嵌套了,和换行、tab无关。换不换行、tab不tab,都不影响页面的构造。 也就是说,HTML不是依附缩进来示意嵌套的,就是看标签的包裹关系。然而,咱们发现有良好的缩进,代码更易读。要求大家都正确缩进标签。 2)、空白折叠景象 HTML中所有的文字之间,如果有空格、换行、tab都将被折叠为一个空格显示。 3)、标签要严格关闭 <html></html><meta />六、html构造新建HTML文件,输出 html:5,按tab键后,主动生成的代码如下: <!DOCTYPE html><html><head> <meta charset="UTF-8"> <title>Document</title></head><body> </body></html>1、文档申明头 任何一个规范的HTML页面,第一行肯定是一个以 <!DOCTYPE html>结尾的这一行,就是文档申明头,DocType Declaration,简称DTD。此标签可告知浏览器文档应用哪种 HTML 或 XHTML 标准。 ...

September 23, 2020 · 1 min · jiezi

定制你私有的前端构建部署Github-CICD

近来手痒,又陷入了自我捣腾的无限循环。 其实事情是这样的,最近阿里云搞活动(嗯,友情打广告),229买了个3年版低配服务器;前端时间写用React + Github Graphql API自定义你的博客, 见识了Github Action的强大,所以就尝试打造自己的前端构建部署工作流程;也许你看到过很多大厂的前端自动构建部署,但鲜有尝试,今天就可以自己动手啦,撸起来吧。 从workflow看流程定制后的Github Action workflow大概长这样: name: Deploy static source to my serveron: push: branches: -masterjobs: build: runs-on: ubuntu-latest steps: - name: checkout uses: actions/checkout@v1 - name: build run: npm install && npm run pub - name: deploy uses: closertb/deploy-static-action@master with: name: 'doddle' token: ${{ secrets.Deploy_Token }} requestUrl: 'http://114.55.38.249:8080/operate/deploy' dist: 'dist' target: 'adminManage'大概流程是这样: 监听master分支的push操作;checkout:新建构建分支build:依赖安装,打包构建deploy:将上一步的构建产物,打包部署到你的服务器静态资源文件中over部署的实现思路构建很简单,就是打包,这种工具很多,什么script-build, roadhog,或自定义webpack。这里主要说部署;其实部署也很简单,看图: 嗯,部署也讲完了。详细实现过程看参见我自定义的action:deploy-static-action 关于上面的几个构建参数: name:一个名字,自己随便啦,根据自己需要token:这个比较重要,服务器的通关口令。这里最好的方式是通过项目的secrets来设置dist:构建打包后的文件夹名,会根据这个文件夹名来获取其中的构建产物, 默认是disttarget:静态资源的目标文件夹名, 默认是distrequestUrl:一个部署API关于上传服务器deploy-static-action其实只做了部署中的构建产物收集,真正的部署其实是依赖requestUrl来实现的,所以要实现 http://114.55.38.249:8080/operate/deploy 这个服务也很重要,你可以重用我的deploy-static-action,但部署API不能,因为这个API是给我的服务器私有定制的。不过我可以提供示例代码参考: 参考代码, deploy.js ...

November 3, 2019 · 1 min · jiezi

前端模块化规范

在前端开发中,在使用webpack等构建工具开发中我们经常使用import * from 或者是require来引入我们需要的模块,那么下面来聊聊前端模块化几种规范。 为什么需要模块化在早期我们写js代码通常是这样子: var a=1;var b=1;function a(){ }function b(){ }这样子会造成命名冲突和全局污染。 同时当我们在同一个页面请求过多的js文件时会造成页面阻塞和http请求过多。 模块化规范由于上面的种种缺点,这时候模块化显得非常重要,前期的模块化通过闭包来达到变量私有化和模块化。 var module=(function(){ var a = 1; var b = function(){ } return { b:b }})()但是这样子还是不能解决加载问题。 这时js模块加载器诞生了,并逐渐形成几种模块化规范。 CommonJS规范CommonJS简介CommonJS规范是在node的模块系统基础上提出来的,也就是CommonJS在服务器中使用,不能在浏览器环境中使用。 CommonJS规范规定,每个模块内部,module变量代表当前模块(一个js文件就是一个模块)。这个变量是一个对象,它的exports属性(即module.exports)是对外的接口。加载某个模块,其实是加载该模块的module.exports属性。 CommonJS代码书写var a = 1;var b = function (){ }module.exports.a = a;module.exports.b = b;上面代码通过module.exports输出变量a和函数b。 require方法引入 var main = require('./main')main.amain.b其实这些代码在我们使用webpack配置的时候经常用到,webpack是运行在node环境中的,他使用的是CommonJS规范。 CommonJS规范特点所有模块都是运行在模块作用域,不会污染全局模块多次运行,只执行一次,然后缓存起来,要让模块重新执行只能清缓存他是按照引入的顺序执行的,也是就是同步执行ADMADM简介上面说了CommonJS的执行是同步进行的,而且浏览器环境中没有CommonJS模块中的必要的字段,所以他不适合浏览器环境,这时候requirejs应势而生,它的诞生逐渐形成了ADM规范。 ADM规范中规定所有模块和依懒项异步加载,这样子js文件就不是一次性引入了。 ADM模块代码书写我们说ADM规范主要使用requirejs。 //a.jsdefine([jquery.js],function($){ var aa = 1 return{ a:aa }})//b.jsdefine([jquery.js],function($){ var bb = 2; return { b:bb }})上面文件a.js和b.js通过define方法定义各自的模块。 ...

October 16, 2019 · 1 min · jiezi

前端代码质量圈复杂度原理和实践

写程序时时刻记着,这个将来要维护你写的程序的人是一个有严重暴力倾向,并且知道你住在哪里的精神变态者。1. 导读你们是否也有过下面的想法? 重构一个项目还不如新开发一个项目...这代码是谁写的,我真想...你们的项目中是否也存在下面的问题? 单个项目也越来越庞大,团队成员代码风格不一致,无法对整体的代码质量做全面的掌控没有一个准确的标准去衡量代 码结构复杂的程度,无法量化一个项目的代码质量重构代码后无法立即量化重构后代码质量是否提升针对上面的问题,本文的主角 圈复杂度 重磅登场,本文将从圈复杂度原理出发,介绍圈复杂度的计算方法、如何降低代码的圈复杂度,如何获取圈复杂度,以及圈复杂度在公司项目的实践应用。 2. 圈复杂度2.1 定义圈复杂度 (Cyclomatic complexity) 是一种代码复杂度的衡量标准,也称为条件复杂度或循环复杂度,它可以用来衡量一个模块判定结构的复杂程度,数量上表现为独立现行路径条数,也可理解为覆盖所有的可能情况最少使用的测试用例数。简称 CC 。其符号为 VG 或是 M 。 圈复杂度 在 1976 年由 Thomas J. McCabe, Sr. 提出。圈复杂度大说明程序代码的判断逻辑复杂,可能质量低且难于测试和维护。程序的可能错误和高的圈复杂度有着很大关系。 2.2 衡量标准代码复杂度低,代码不一定好,但代码复杂度高,代码一定不好。圈复杂度代码状况可测性维护成本1 - 10清晰、结构化高低10 - 20复杂中中20 - 30非常复杂低高>30不可读不可测非常高3. 计算方法3.1 控制流程图控制流程图,是一个过程或程序的抽象表现,是用在编译器中的一个抽象数据结构,由编译器在内部维护,代表了一个程序执行过程中会遍历到的所有路径。它用图的形式表示一个过程内所有基本块执行的可能流向, 也能反映一个过程的实时执行过程。 下面是一些常见的控制流程: 3.2 节点判定法有一个简单的计算方法,圈复杂度实际上就是等于判定节点的数量再加上1。向上面提到的:if else 、switch case 、 for循环、三元运算符等等,都属于一个判定节点,例如下面的代码: function testComplexity(*param*) { let result = 1; if (param > 0) { result--; } for (let i = 0; i < 10; i++) { result += Math.random(); } switch (parseInt(result)) { case 1: result += 20; break; case 2: result += 30; break; default: result += 10; break; } return result > 20 ? result : result;}上面的代码中一共有1个if语句,一个for循环,两个case语句,一个三元运算符,所以代码复杂度为 4+1+1=6。另外,需要注意的是 || 和 && 语句也会被算作一个判定节点,例如下面代码的代码复杂为3: ...

October 14, 2019 · 4 min · jiezi

MVVM框架和MVC框架

之前不懂前端的工程化开发(以前都是那种一个html混合一大堆代码的那种开发)。最近看了一下MVVM前端架构,对比MVC,说一下自己的理解:MVVM架构本质上仍旧是MVC的变种,或者说,是MVC中VIEW的进一步MVC架构的设计。 1、view代表视图,即我们所看到的那一层。MVC架构中的view较为宽泛,泛指所有前端的组件和前端的逻辑,而随着前端所能承担的视图之外的数据处理能力越来越强,view层也愈来愈远离其本来的划分意义,MVVM架构便是对MVC架构中的view层以其自身的架构设计思路进行的进一步划分。MVVM架构中view层即不具有视图以外的数据操作功能的MVC架构view层; 2、model层即模型层,但从功能上来说,应该叫“模型”处理层。“模型”必然是某种具象的抽象表示,程序可以通过调用改模型,然后加以操作(例如赋值),使得“模型”从一种抽象再次变为具象。MVC架构中model层多以指代对数据库中数据的抽象(当然包括其他)进行操作,操作方法汇聚到这一层。而MVVM架构中多以是指代view层中具体视图模块(dom)所承担任务的具体方法的抽象(也就是说前端的方法聚合在同一层); 3、controller层和view-model层虽说名称不同,但作用相同,即是起到某种“承接”的作用,之所以名称不同,是因为二者的“承接”手段存在差异。controller层是应用程序中处理用户交互的部分。通常控制器负责从视图读取数据,控制用户输入,并向模型发送数据,而view-model则相对来说功能较为单一,仅仅是通过设置,让自身监听视图dom控件、绑定model层方法,让二者的内容相互对应。

October 9, 2019 · 1 min · jiezi

从零搭建webpack4reacttypescripteslint脚手架三

处理静态资源js的打包基本处理完了,还有图片、音频等静态资源需要处理。 依然先装依赖: $ npm i -D url-loader file-loader$ npm i -D @svgr/webpack # 顺带支持一下导入svg图片增加webpack配置: // webpack.base.js{ test: /\.svg$/, use: ['@svgr/webpack']},{ test: /\.(jpg|jpeg|bmp|png|webp|gif)$/, loader: 'url-loader', options: { limit: 8 * 1024, // 小于这个大小的图片,会自动base64编码后插入到代码中 name: 'img/[name].[hash:8].[ext]', outputPath: config.assetsDirectory, publicPath: config.assetsRoot }},// 下面这个配置必须放在最后{ exclude: [/\.(js|mjs|ts|tsx|less|css|jsx)$/, /\.html$/, /\.json$/], loader: 'file-loader', options: { name: 'media/[path][name].[hash:8].[ext]', outputPath: config.assetsDirectory, publicPath: config.assetsRoot }}tips: 生产环境需要合理使用缓存,需要拷贝一份同样的配置在webpack.prod.js中,并将name中的hash改为contenthash接下来我们要把public目录里除了index.html以外的文件都拷贝一份到打包目录中: 安装依赖: $ npm i -D copy-webpack-plugin增加配置: // webpack.base.jsconst CopyWebpackPlugin = require('copy-webpack-plugin');plugins: [ // ...other plugins new CopyWebpackPlugin([ { from: 'public', ignore: ['index.html'] } ])]提取公共模块,拆分代码有些模块是公共的,如果不把他拆分出来,那么他会在每一个被引入的模块中出现,我们需要优化与此相关的配置。 ...

September 10, 2019 · 2 min · jiezi

从零搭建webpack4reacttypescripteslint脚手架一

引言项目github仓库地址: https://github.com/mecoepcoo/ts-react-boilerplate这个系列的文章主要讲述如何从一个空目录建立webpack+react+typescript+eslint脚手架,书写此文时各主要工具的版本为: webpack v4react v16.9typescript v3.5babel v7eslint v6.2本文涉及的内容大致包含: webpack的配置对静态资源(图片,模板等)的处理使react项目支持typescript,eslint,prettier等工具优化webpack配置,减小代码的体积支持不同的css预处理器(less,sass等)一套好用的样式方案使项目支持多个环境切换(开发,测试,预发布,生产等)使用规则来自动约束代码规范优化开发体验一些优化项目性能的建议阅读这个系列的文章需要具备的条件: 你使用过vue,react或angular等任意一种前端框架你了解过vue-cli,create-react-app,angular-cli等任意一种脚手架生成工具你了解webpack的基本原理或用法你有生产环境代码的开发经验,了解生产环境中的代码与自娱自乐代码的区别Why not create-react-app?笔者使用CRA新建项目时,觉得自定义程度不够。尝试过 react-app-rewired + customize-cra 的方案,还是觉得异常繁琐,而且会消耗额外的维护精力,鲁迅说:青年应当有朝气,敢作为,遂自行搭建一个 boilerplate 。 初始化目录我们要从一个空目录开始,先新建这个目录,做一些必要的初始化工作: $ mkdir my-react$ cd my-react$ git init$ npm init新建如下目录结构: react-project config 打包配置public 静态文件夹 index.htmlfavicon.icosrc 源码目录规范git提交协作开发时,git提交的内容如果没有规范,就不好管理项目,我们用 husky + commitlint 来规范git提交。 我们先在根目录下建立 .gitignore 文件,忽略不需要要的文件。 然后安装工具: $ npm i -D husky$ npm i -D @commitlint/clihusky 会为 git 增加钩子,在 commit 时执行一系列操作,commitlint 可以检查 git message 是否符合规则。在 package.json 中增加配置如下: "husky": { "hooks": { "commit-msg": "commitlint -E HUSKY_GIT_PARAMS" }},在根目录新建文件 .commitlintrc.js,根据具体情况配置: ...

September 9, 2019 · 3 min · jiezi

从零搭建webpack4reacttypescripteslint脚手架二

完善webpack打包配置有了webpack的基础配置,还不足以支持打生产环境能够使用的包,我们还需要增加一些配置。 首先,每次打包前最好能把上一次生成的文件删除,这里可以用clean-webpack-plugin插件实现: $ npm i -D clean-webpack-plugin然后修改webpack基础配置: // webpack.base.jsconst { CleanWebpackPlugin } = require('clean-webpack-plugin');module.exports = { plugins: [ new CleanWebpackPlugin(), ]}在生产环境,我们希望部署新版本后能够丢弃缓存,又希望保留没有被改动的文件的缓存,而在开发环境,我们希望完全不使用缓存,因此我们需要在当前配置的基础上,分别扩展生产和开发两套配置。 // webpack.prod.js 生产环境打包配置const merge = require('webpack-merge');const baseWebpackConfig = require('./webpack.base');const { CleanWebpackPlugin } = require('clean-webpack-plugin');module.exports = merge.smart(baseWebpackConfig, { mode: 'production', devtool: sourceMapsMode, output: { filename: 'js/[name].[contenthash:8].js', // contenthash:只有模块的内容改变,才会改变hash值 }, plugins: [ new CleanWebpackPlugin(), ]}// webpack.dev.js 开发环境的配置const merge = require('webpack-merge');const baseWebpackConfig = require('./webpack.base');const config = require('./config');module.exports = merge.smart(baseWebpackConfig, { mode: 'development', output: { filename: 'js/[name].[hash:8].js', publicPath: config.publicPath // 这里可以省略 }, module: { rules: [ { oneOf: [] } ] },}接下来我们编辑build.js,让打包程序真正能够运行起来: ...

September 9, 2019 · 2 min · jiezi

可能是你见过最完善的微前端解决方案

原文链接:https://zhuanlan.zhihu.com/p/... Techniques, strategies and recipes for building a modern web app with multiple teams using different JavaScript frameworks. — Micro Frontends前言TL;DR 想跳过技术细节直接看怎么实践的同学可以拖到文章底部,直接看最后一节。 目前社区有很多关于微前端架构的介绍,但大多停留在概念介绍的阶段。而本文会就某一个具体的类型场景,着重介绍微前端架构可以带来什么价值以及具体实践过程中需要关注的技术决策,并辅以具体代码,从而能真正意义上帮助你构建一个生产可用的微前端架构系统。 而对于微前端的概念感兴趣或不熟悉的同学,可以通过搜索引擎来获取更多信息,如 知乎上的相关内容, 本文不再做过多介绍。 两个月前 Twitter 曾爆发过关于微前端的“热烈”讨论,参与大佬众多(Dan、Larkin 等),对“事件”本身我们今天不做过多评论(后面可能会写篇文章来回顾一下),有兴趣的同学可以通过这篇文章了解一二。 微前端的价值微前端架构具备以下几个核心价值: 技术栈无关主框架不限制接入应用的技术栈,子应用具备完全自主权独立开发、独立部署子应用仓库独立,前后端可独立开发,部署完成后主框架自动完成同步更新独立运行时每个子应用之间状态隔离,运行时状态不共享微前端架构旨在解决单体应用在一个相对长的时间跨度下,由于参与的人员、团队的增多、变迁,从一个普通应用演变成一个巨石应用(Frontend Monolith)后,随之而来的应用不可维护的问题。这类问题在企业级 Web 应用中尤其常见。 针对中后台应用的解决方案中后台应用由于其应用生命周期长(动辄 3+ 年)等特点,最后演变成一个巨石应用的概率往往高于其他类型的 web 应用。而从技术实现角度,微前端架构解决方案大概分为两类场景: 单实例:即同一时刻,只有一个子应用被展示,子应用具备一个完整的应用生命周期。通常基于 url 的变化来做子应用的切换。多实例:同一时刻可展示多个子应用。通常使用 Web Components 方案来做子应用封装,子应用更像是一个业务组件而不是应用。本文将着重介绍单实例场景下的微前端架构实践方案(基于 single-spa),因为这个场景更贴近大部分中后台应用。 行业现状传统的云控制台应用,几乎都会面临业务快速发展之后,单体应用进化成巨石应用的问题。为了解决产品研发之间各种耦合的问题,大部分企业也都会有自己的解决方案。笔者于17年底,针对国内外几个著名的云产品控制台,做过这样一个技术调研: 产品架构(截止 2017-12)实现技术google cloud纯 SPA主 portal angularjs,部分页面 angular(ng2)。aws纯 MPA 架构首页基于 angularjs。各系统独立域名。七牛SPA & MPA 混合架构入口 dashboard 及 个人中心模块为 spa,使用同一 portal 模块(AngularJs(1.5.10) + webpack)。其他模块自治,或使用不同版本 portal,或使用其他技术栈。又拍云纯 SPA 架构基于 angularjs 1.6.6 + ui-bootstrap。控制台内容较简单。ucloud纯 SPA 架构angularjs 1.3.12MPA 方案的优点在于 部署简单、各应用之间硬隔离,天生具备技术栈无关、独立开发、独立部署的特性。缺点则也很明显,应用之间切换会造成浏览器重刷,由于产品域名之间相互跳转,流程体验上会存在断点。 ...

August 20, 2019 · 3 min · jiezi

前端工程化-图片自动压缩

团队开启了一个新项目,希望能在原来项目的工程化基础上再进一步,于是想到了图片自动压缩。 这里的图片自动压缩并不是在webpack构建阶段压缩,而是在git commit的时候进行。 pre-commitpre-commit 是git hook 众多钩子中的一个,在每次 git commit 前执行,可以是shell等任何可执行的脚本文件,通过返回0 or 1 来表示commit是否通过。在bash中,非零返回值代表失败. pre-commit.js市面上有许多通过node.js来封装pre-commit的npm包,我使用了功能最精简的pre-commit.js , 按README在package.json中简单配置后,可以看到 .git/hooks/pre-commit脚本如下所示: #!/bin/bash./node_modules/pre-commit/hookRESULT=$?[ $RESULT -ne 0 ] && exit 1exit 0pre-commit.js重写了原来的示例钩子,在git commit时执行自己的/hook脚本. hook bash# 之前的代码主要确认node环境# git commit --dry-run 的情况(测试性提交,执行动作不产生结果),不对提交作任何处理if [[ $* == *--dry-run* ]]; then if [[ -z "$BINARY" ]]; then # 但还是要检查下NODE是否存在,不存在返回非零值代表失败 exit 1 fielse # 用NODE 执行 'pre-commit/index.js' 模块 "$BINARY" "$("$BINARY" -e "console.log(require.resolve('pre-commit'))")"fi最终pre-commit/index.js 将读取package.json中配置好的路径来执行指定的脚本。 pre-commit/index.js// 执行自定义钩子脚本的核心部分Hook.prototype.run = function runner() { var hooked = this; (function again(scripts) { // 脚本数组为空,则返回0,执行本次 commit . if (!scripts.length) return hooked.exit(0); // 否则弹出一个脚本来执行 var script = scripts.shift(); // 使用的是异步执行子进程方法child_process.spawn. spawn(hooked.npm, ['run', script, '--silent'], { env: process.env, cwd: hooked.root, stdio: [0, 1, 2] }).once('close', function closed(code) { // 监听了执行结束的close事件,递归继续执行下一个脚本 if (code) return hooked.log(hooked.format(Hook.log.failure, script, code)); again(scripts); }); })(hooked.config.run.slice(0));};从上述代码中可以看出,即使我们定义的钩子脚本有异步处理逻辑也是可以的,因为整个脚本都是新开了一个异步的子进程来执行的,通过监听close事件来确认异步逻辑执行完毕,最终exit(0)通知成功,执行commit。 ...

August 18, 2019 · 2 min · jiezi

实现一个自己的日志处理库并发布到npm

前言不折腾的前端不是一个好的前端,最近在搭建公司内部工具以及组件库,使用npm进行管理,所以学习一下如何创建一个属于自己的JavaScript库,并发布成npm模块。步骤创建账号点击进入npm官网 右上角进行注册 创建项目一路回车或者根据内容填写相关信息后,项目就创建好了。 package.json内容如下: 新建index.js文件(function (root, factory) { 'use strict'; if(typeof define === 'function' && define.amd){ define('log', [], factory); }else{ root['log'] = factory(); }})(this ? this : window, function(){ 'use strict'; const OFF = Number.MAX_VALUE; const DEBUG = 10; const INFO = 20; const WARN = 30; const ERROR = 40; function LogUtil(){ this.consoleLog = window.console; this._level = OFF; } LogUtil.prototype.setLevel = function(level){ if(!level){ return; } level = String(level).toLowerCase(); if(level === 'info'){ level = INFO; }else if(level === 'warn'){ level = WARN; }else if(level === 'error'){ level = ERROR; }else if(level === 'debug'){ level = DEBUG; }else{ level = OFF; } this._level = level; }; LogUtil.prototype.runLog = function(level, methodName, msg){ if(!level){ return; } var form = [new Date().toLocaleString(), level.toLowerCase(), methodName, msg].join('|'); if(level.toLowerCase() === 'debug' && this._level <= DEBUG){ this.consoleLog.debug(form); }else if(level.toLowerCase() === 'info' && this._level <= INFO){ this.consoleLog.info(form); }else if(level.toLowerCase() === 'warn' && this._level <= WARN){ this.consoleLog.warn(form); }else if(level.toLowerCase() === 'error' && this._level <= ERROR){ this.consoleLog.error(form); } }; return LogUtil;});到这里一个简单的包就创建好了。如果想再完善一下的话,还可以在包根目录下创建README.md文件,里面可以写一些关于包的介绍信息,最后发布后会展示在NPM官网上。 ...

July 9, 2019 · 1 min · jiezi

Vue组件库工程探索与实践构建工具篇

我们团队近期发布了移动端 Vue 组件库 NutUI 的 2.0 版,2.0 不是 1.0 的升级,而是一个全新的组件库。从 1.0 到 2.0 一路走来,我们积累了一些 Vue 组件库的开发经验,接下来的一段时间,我们将以系列文章的形式与大家进行分享,欢迎大家关注。作为《Vue组件库工程探索与实践》系列文章开篇之作,我们从“盘古开天地”说起吧。 从当年的静态页面到如今的 Web App,前端工程越来越复杂,对于一个稍大些的前端项目来说,代码都写在一起难以维护,团队分工协作也成问题。根据软件工程领域的经验,解决这些问题的一个可行思路就是代码的模块化,即对代码按功能模块进行分拆,封装成组件,而反过来讲,组件就是指能完成某个特定功能的独立的、可重用的代码块。 把一个大的应用分解成若干小的组件,而每个组件只需要关注于某个小范围的特定功能,但是把组件组合起来,就能构成一个功能庞大的应用。组件化的网页开发也是如此,就像搭积木,各个组件拼接在一起就组成了一个完整的页面。 组件化开发可大大降低代码耦合度、提高代码的可维护性和开发效率,同时有利于团队分工协作和降低开发成本。这种开发模式已日渐流行起来。 当前,前端开发领域最流行的三大框架 Vue、React、Angular 都推崇组件化开发,组件是这些框架中极为重要的概念和功能。 以 Vue.js 来说,组件 (Component) 可以说是其最强大的功能,它可以扩展 HTML 元素,封装可重用的代码。Vue.js 的组件系统让我们可以用这些独立可复用的小组件来构建大型 Vue 应用,几乎任意类型的 Vue 应用的界面都可以抽象为一个组件树。 如果我们把日常应用开发中常用的组件累积起来,后续的项目就可以复用这些组件,这对提高开发效率、降低开发成本有重要意义。 因此,一个前端团队拥有一个常用框架的组件库是十分必要的。 模块化与构建工具组件库自身就是一个大的工程,需要按照模块化开发思想进行模块划分。通常,在一个组件库里,组件、组件的样式文件、配置文件…都是模块,而最终我们需要把这些模块组合成一个完整的组件库文件,承担这种组装工作的就是打包构建工具。当下主流的库构建工具主要有 Rollup 和 Webpack 等。在说这些模块打包构建工具之前,我们先来了解一下目前主流的 JavaScript 模块化方案。 JavaScript 语言一直以来饱受诟病的一个地方就是它的语言标准里没有模块(module)体系,这对开发大型的、复杂的项目形成了巨大障碍。直到 ES6 时期,才在语言标准层面实现模块功能(ES6 Module)。在 ES6 之前,业界流行的是社区制定的一些模块加载方案,如 CommonJS 和 AMD 。而 ES6 Module 作为官方规范,且浏览器端和服务器端通用,未来一定会一统天下,但由于 ES6 Module 来的太晚,受限于兼容性等因素,可以预见的是今后一段时期内,多种模块化方案仍会共存。 ES6 Modue 规范:JavaScript 语言标准模块化方案,浏览器和服务器通用,模块功能主要由 export 和 import 两个命令构成。export 用于定义模块的对外接口,import 用于输入其他模块提供的功能。CommonJS 规范:主要用于服务端的 JavaScript 模块化方案,Node.js 采用的就是这种方案,所以各种 Node.js 环境的前端构建工具都支持该规范。CommonJS 规范规定通过 require 命令加载其他模块,通过 module.exports 或者 exports 对外暴露接口。AMD 规范:全称是 Asynchronous Modules Definition,异步模块定义规范,一种更主要用于浏览器端的 JavaScript 模块化方案,该方案的代表实现者是 RequireJS,通过 define 方法定义模块,通过 require 方法加载模块。一些“上年纪”的国内前端老艺人们可能还会提到 CMD 规范,它是 SeaJS 在推广过程中对模块定义的规范化产出,只是 SeaJS 并未实现国际化,且项目在2015年就已宣布停止维护了,算不上当前主流模块化方案。介绍完主流模块化规范,我们再回过头来看 Rollup 和 Webpack 这两个模块打包构建工具。 ...

July 5, 2019 · 3 min · jiezi

关于前端Vue框架的知识点

最近有时间,整理一下Vue的知识点,很多都是面试常见的 1、Vue的生命周期如果你能理解了这张图,也就对Vue的生命周期有了一个大致的了解。 vue生命周期总共分为8个阶段 创建前/后,载入前/后,更新前/后,销毁前/后。 创建/前后:在beforeCreated阶段,vue实例的挂载元素el还没有。在beforeCreated阶段可以添加loading事件,在created阶段发起后端请求,拿回数据载入前/后:在beforeMount阶段,vue实例的$el和data都初始化,但是挂载之前为虚拟的dom节点,data.message还未替换,页面无重新渲染。在mounted阶段,vue实例挂载完成,data.message成功渲染。更新前/后:当data变化时,会触发beforeUpdate和updated方法。销毁前/后:在执行destroy方法后,对data的改变不会再触发周期函数,说明此时vue实例已经解除了事件监听以及和dom的绑定,但是dom结构依然存在。第一次页面加载会触发哪几个钩子?答:会触发 下面这几个beforeCreate, created, beforeMount, mounted 。DOM 渲染在 哪个周期中就已经完成?答:DOM 渲染在 mounted 中就已经完成了。2、对MVVM开发模式的理解?MVVM分为Model、View、ViewModel三者。 Model 代表数据模型,数据和业务逻辑都在Model层中定义;View 代表UI视图,负责数据的展示;ViewModel 负责监听 Model 中数据的改变并且控制视图的更新,处理用户交互操作;Model 和 View 并无直接关联,而是通过 ViewModel 来进行联系的,Model 和 ViewModel 之间有着双向数据绑定的联系。这种模式实现了 Model 和 View 的数据自动同步,因此开发者只需要专注对数据的维护操作即可,而不需要自己操作 dom。 3、VUE数据双向绑定原理答:vue.js 是采用数据劫持结合发布者-订阅者模式的方式,通过 Object.defineProperty()来劫持各个属性的setter,getter,在数据变动时发布消息 给订阅者,触发相应的监听回调。 具体步骤: 第一步:需要observe的数据对象进行递归遍历,包括子属性对象的属性,都加上setter和getter这样的话,给这个对象的某个值赋值,就会触发setter,那么就能监听到了数据变化 第二步:compile解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图 第三步:Watcher订阅者是Observer和Compile之间通信的桥梁,主要做的事情是:1、在自身实例化时往属性订阅器(dep)里面添加自己 2、自身必须有一个update()方法 3、待属性变动dep.notice()通知时,能调用自身的update()方法,并触发Compile中绑定的回调,则功成身退。 第四步:MVVM作为数据绑定的入口,整合Observer、Compile和Watcher三者,通过Observer来监听自己的model数据变化,通过Compile来解析编译模板指令,最终利用Watcher搭起Observer和Compile之间的通信桥梁,达到数据变化->视图更新;视图交互变化(input)->数据model变更的双向绑定效果。详细请看:Vue 双向数据绑定原理详细分析 4、v-if 和 v-show 有什么区别?v-show 仅仅控制元素的显示方式,将 display 属性在 block 和 none 来回切换;而v-if会控制这个 DOM 节点的存在与否。当我们需要经常切换某个元素的显示/隐藏时,使用v-show会更加节省性能上的开销;当只需要一次显示或隐藏时,使用v-if更加合理。5、父子组件之间的传值和通信?父组件向子组件传值:1)子组件在props中创建一个属性,用来接收父组件传过来的值;2)在父组件中注册子组件;3)在子组件标签中添加子组件props中创建的属性;4)把需要传给子组件的值赋给该属性子组件向父组件传值:1)子组件中需要以某种方式(如点击事件)的方法来触发一个自定义的事件;2)将需要传的值作为$emit的第二个参数,该值将作为实参传给响应事件的方法;3)在父组件中注册子组件并在子组件标签上绑定自定义事件的监听。6、Vue的路由实现:hash模式 和 history模式hash模式:在浏览器中符号“#”,#以及#后面的字符称之为hash,用window.location.hash读取;特点:hash虽然在URL中,但不被包括在HTTP请求中;用来指导浏览器动作,对服务端安全无用,hash不会重加载页面。hash 模式下,仅 hash 符号之前的内容会被包含在请求中,如 http://www.xxx.com,因此对于后端来说,即使没有做到对路由的全覆盖,也不会返回 404 错误。history模式:history采用HTML5的新特性;且提供了两个新方法:pushState(),replaceState()可以对浏览器历史记录栈进行修改,以及popState事件的监听到状态变更。history 模式下,前端的 URL 必须和实际向后端发起请求的 URL 一致,如 http://www.xxx.com/items/id。后端如果缺少对 /items/id 的路由处理,将返回 404 错误。Vue-Router 官网里如此描述:“不过这种模式要玩好,还需要后台配置支持……所以呢,你要在服务端增加一个覆盖所有情况的候选资源:如果 URL 匹配不到任何静态资源,则应该返回同一个 index.html 页面,这个页面就是你 app 依赖的页面。”7、vue路由的钩子函数首页可以控制导航跳转,beforeEach,afterEach等,一般用于页面title的修改。一些需要登录才能调整页面的重定向功能。beforeEach主要有3个参数to,from,next:to:route即将进入的目标路由对象,from:route当前导航正要离开的路由next:function一定要调用该方法resolve这个钩子。执行效果依赖next方法的调用参数。可以控制网页的跳转。8、vue等单页面应用及其优缺点优点:Vue 的目标是通过尽可能简单的 API 实现响应的数据绑定和组合的视图组件,核心是一个响应的数据绑定系统。MVVM、数据驱动、组件化、轻量、简洁、高效、快速、模块友好。缺点:不支持低版本的浏览器,最低只支持到IE9;不利于SEO的优化(如果要支持SEO,建议通过服务端来进行渲染组件);第一次加载首页耗时相对长一些;不可以使用浏览器的导航按钮需要自行实现前进、后退。9、vue中 key 值的作用? ...

July 3, 2019 · 1 min · jiezi

前端历史演变

在选择学习Webpack之前,我们先了解一下前端整个发展历程。 2014年初,我加入互联网开发行业,随没经历前端刀工火种的时态,5年的时间,前端技术的百家齐放很是眼花缭乱。我也从套页面后端工程师、jquery写效果到现在的小程序、node、vue转变成一个纯前端。现在回顾一下前端到底发生了哪些历史变化。 静态页面 1990~2005互联网发展早期,前端只负责写静态页面,纯粹的展示功能,JavaScript的作用也只是增加一些特殊效果。这种静态页面不能读取数据库,为了使Web更加充满活力,以PHP、JSP、ASP.NET为主的动态语言相继诞生。这使页面能够获取数据并不断更新,是前后端混合开发模式开端,所有的前端代码和前端数据都是后端生成的,随着后端代码的庞大和逻辑越来越复杂,相继的MVC发展起来。这时后端大多采用MVC模式开发,前端只是后端MVC中的V(视图);从web的诞生到2005,一直处在_后端重前端轻_的状态。 AJAX阶段 20052004年AJAX技术的诞生改变了前端的发展历史。以Gmail和Google地图这样革命性的产品出现,使得开发者发现,前端的作用不仅仅是展示页面,可以管理数据和用户互动。解决一些糟糕的用户体验,前端页面要想获取后台数据需要刷新整个页面。依稀记得前几年,依托强大的Jquery,一页面的javascript代码使用ajax发送请求渲染DOM的情景。前端开始慢慢向后端靠拢。 NODEJS 的爆发 20092009年Ryan Dahl利用Chrome的V8引擎打造了基于事件循环的异步I/O框架。NODE的诞生,使javascript在服务端的无限可能,更重要的是它构建了一个庞大的生态系统。 2010年1月,NPM作为node的包管理系统首次发布。开发人员可以依照规范编写nodejs模块,发布到npm上,供其他开发人员下载使用。截止目前2019年6月8日,NPM包数量有1,003,262,是世界上最大的包模块管理系统。 Node.js 给开发人员带来了无穷的想象,JavaScript 大有一统天下的趋势。 前端MV**架构阶段 2010随着 HTML5 小程序 的流行,前端再也不是人们眼中的小玩意了,应用功能开发逐步迁移到了前端,前端的代码逻辑逐渐变得复杂起来。2010年10月Backbone MVP架构发布。2010年10月Angular MVC->MVVM2013年05月React开源 MVVM2014年07月Vue MVVM 随着这些 MV* 框架的出现,网页逐渐由 Web Site 演变成了 Web App,最终导致了复杂的单页应用( Single Page Application)的出现。随着 SPA 的兴起,2010年后,前端工程师从开发页面(切模板),逐渐变成了开发“前端应用”(跑在浏览器里面的应用程序)。 javascript 开发App随着 iOS 和 Android 等智能手机的广泛使用,移动浏览器也逐步加强了对 HTML5 特性的支持力度。 Web APP,即移动端的网站。一般泛指 SPA(Single Page Application)模式开发出的网站。将页面部署在服务器上,然后用户使用各大浏览器访问,不是独立APP,无法安装和发布。 Hybrid App,即混合开发,也就是半原生半Web的开发模式,有跨平台效果,实质最终发布的仍然是独立的原生APP。 React Native App,Facebook发起的开源的一套新的APP开发方案,使用JS+部分原生语法来实现功能。 May 7, 2019谷歌发布 Flutter for web,正式宣布 Flutter 成为全平台框架,支持手机、Web、桌面电脑和嵌入式设备。现在学跨平台应用开发,第一个要看的可能不是 React Native,而是 Flutter。 ...

July 3, 2019 · 1 min · jiezi

crossenv使用以及根据环境打包

关于之前的项目打包都是靠手动去改环境变量(纯属沙雕行为),随着项目越来越多,每一个项目打包都要去改这个变量的话真的是太蛋疼了,所以研究了一下webpack打包以及node env主要这样做有什么好处? publicPath: process.env.APP_ENV === 'production' ? 'https://cdn.xxxx.com/brand-mall-chengdong/' : '/', outputPath: './brand-mall-chengdong',之前都是每次打包手动修改这个静态资源的地址,修改之后根据环境变量自动区分 第一步,安装cross-envyarn add cross-env@5.1.1 cross-port-killer@1.0.1 什么是cross-env?解:当您使用NODE_ENV=production类似设置环境变量时,大多数Windows命令提示将会阻塞 。(例外是Windows上的Bash,它使用本机Bash。)同样,Windows和POSIX命令如何利用环境变量也有所不同。使用POSIX,您可以使用:$ENV_VAR 和您使用的Windows %ENV_VAR%。第二步,修改package.json文件 "build": "cross-env APP_ENV=production umi build", "build:test": "cross-env APP_ENV=test umi build",新增一条如上命令,当执行npm run build时,设置proess.env.APP_ENV为production ,同理设置为test.然后在config.js文件中即可根据这个变量设置相应的路径。 关于文章首发于cross-env使用以及根据环境打包

June 30, 2019 · 1 min · jiezi

前端工程化初探

前言目前我所理解的前端工程化, 顾名思义, 就是让前端项目具备工程特性: 满足规范化,流程化,自动化等要求, 随着大前端时代下前端发展的速度越来越快, 项目也日渐复杂起来, 前端项目管理也越来庞杂, 造成项目维护性差,开发效率低等弊病吧; 在接手了公司前端业务一段时间后, 也感觉存在这些问题, 目前正在尝试将前端工程化这一块做起来... 前端工程按项目阶段划分可以分为: 项目初期构建(这是忽略项目预研,技术选型等,单从工程开发的角度来看); 中期的项目开发; 后期的项目测试,优化以及持续集成和部署. 其实就是某个项目从0到1的过程, 一个约束团队统一行为的过程. 项目构建阶段项目初期要制定好规范, 用来约束团队人员的开发行为,便于管理,目前想到下面几点; 1.git流程规范 git分支管理: release: 线上发布分支, pre-release: 预发布; dev:开发分支; xxx-dev: 个人开发分支 commit规范管理: 描述以功能模块为单位进行提交 2.目录结构规范 以vue项目为例, 目前项目都是以vue脚手架进行搭建的,然后在脚手架的基础上进行相关扩展配置; 1.项目采用 vue + webpack模板; 运行命令 vue init webpack project-name即可, 根据需求选择配置项2.创建目录和相应文件 2.1-构建目录build, 可以分为测试,预发布,线上三种环境配置,看具体需求 2.2-环境变量配置config, 配置不同环境下接口,或者其他变量 2.3-项目文档readme, 项目规范及结构概述, 这个比较重要 其他文件夹都大同小异; 都在readme中写出来了, 暂不做概述; 后面会对常用的webpack相关配置做一些总结项目开发阶段到了开发阶段后, 我们需要遵守的就是组件化和模块化的开发思想; 按照项目预先制定的规范进行组件化和模块化开发; 比如我们拿到UI稿图后, 先将页面看作一个大型组件, 然后拆成若干中型组件, 发现相同的地方我们继续拆成公共的基础组件...这就是组件化开发;模块化呢? 模块化就是将一个大文件拆分成相互依赖的小文件,再进行统一的拼装和加载。我们平常使用的ES6模块导入系统以及完全够用了; 这样我们可以将js, css, 静态资源等分成不同模块文件便于管理, 这些一般都会写在项目规范中;其实这一个过程,我觉得更重要是团队成员按照既定的项目规范去落实自己的开发任务... ...

June 23, 2019 · 1 min · jiezi

使用-NodeJS-构建现代化的命令行工具

前言这是一篇关于如何使用 NodeJS 构建高性能、高可读性的现代化命令行工具的博客。每当我们想要创建一个基于 NodeJS 的命令行工具时,就会衍生出一堆问题需要解决,比如如何准备开发环境,如何打包转译代码,如何使代码在转译后保持可调用的状态同时尽可能的压缩体积,以及怎样设计项目分配 Command 与 Option 等等,这会浪费巨大的时间,而且并非一定有成果。这时你可以注意到社区几乎所有的命令行工具都是自成一派,并没有严谨的框架或约定约束,也无所谓的最佳实践,这使想要特别是第一次想要开发命令行工具的开发者望而却步,或是几番努力最后却不尽如人意。 举个例子来说,腾讯的 omi 是一个有众多使用者的框架,但其命令行工具 omi / omi-cli 却让人贻笑大方。仅一些简单的下载和创建模板的任务,造出长篇大论的文件不说,下载时依赖数千,包的体积巨大,整体项目毫无设计几乎是随心所欲、天马行空,这就是开发者本身并不擅长此道,只学会了 糊屎。(何谓 糊屎,参阅 JS 优雅指南 2) FUNC 的出现就是为了解决这些问题。func 本身的实现参阅了社区内诸多基于 NodeJS 的命令行工具的优秀实现,与流行的框架设计思路相结合,以优雅的设计、小体积、高性能 等为目标,同时关注开发者体验,大幅度的提升了命令行工具项目的可扩展性与可读性,几乎是如今 NodeJS 社区中开发命令行工具的最优解。我们可以尝试使用 func 构建一个命令行工具。 构建项目在以前流行的一些命令行参数解析的库中,我们在构建项目前需要准备大量的脚本与配置,甚至还要解决文件权限、bin、代码转译等等问题,但使用 func,我们可以仅通过一行命令初始化项目: npm init func项目初始化后进入文件夹,随机使用 npm install 或 yarn 安装依赖,现在就可以正式开发了。可以注意到,func 的项目模板中为我们准备了 start 与 build 2 个脚本,它们都是由 func-service 驱动的,帮助你一键切换开发与生产模式,我们所要做的就是专注于命令行逻辑本身,实现逻辑就够了。 实现逻辑我们可以随意的创建一个类,当它被加上 Command 注解时这就是一个命令,而被加上 Option 注解时就会转变为一个选项: import { Command } from 'func'@Command({ name: 'test' })export class Test {}在命令行中运行 <YOUR NAME> test 时,Test 类将会被调用。选项也是同理。你可以在 constructor 中开始自己的代码,这和你在任何地方写 NodeJS 没有什么不同,当你觉得差不多的时候,运行 npm build 就可以将它打包,一切就是这么简单。 ...

June 23, 2019 · 1 min · jiezi

JavaScript设计模式整理

写在前面设计模式是程序员通识知识,熟练掌握并使用各种设计模式,可以体现一个程序员的工程开发水平。我花了几天时间,重温并整理了30多种设计模式,以JavaScript为示例语言。下面我会列出一些常用的设计模式说明及示例,更全面的内容见:https://github.com/yzsunlei/javascript-design-mode 什么是设计模式?一个模式就是一个可重用的方案,可应用于在软件设计中的常见问题。另一种解释就是一个我们如何解决问题的模板 - 那些可以在许多不同的情况里使用的模板。 设计模式的分类:创建型设计模式:1、简单工厂模式 2、工厂方法模式 3、抽象工厂模式 4、建造者模式 5、原型模式 6、单例模式 结构型设计模式:7、外观模式8、适配器模式9、代理模式10、装饰者模式11、桥接模式12、组合模式13、享元模式 行为型设计模式:14、模板方法模式15、观察者模式16、状态模式17、策略模式18、职责链模式19、命令模式20、访问者模式21、中介者模式22、备忘录模式23、迭代器模式24、解释器模式 技巧型设计模式:25、链模式26、委托模式27、数据访问对象模式28、节流模式29、简单模板模式30、惰性模式31、参与者模式32、等待者模式 架构型设计模式:33、同步模块模式34、异步模块模式35、Widget模式36、MVC模式37、MVP模式38、MVVM模式 备注:该分类借鉴于《JavaScript设计模式-张容铭》工厂方法模式:通过对产品类的抽象使其创建业务主要负责用于创建多类产品的实例。 // 安全模式创建的工厂类var Factory = function(type, content) { if (this instanceof Factory) { // 保证是通过new进行创建的 var s = new this[type](content); return s; } else { return new Factory(type, content); }};// 工厂原型中设置创建所有类型数据对象的基类Factory.prototype = { Java: function(content) { }, Php: function(content) { }, JavaScript: function(content) { }};原型模式:用原型实例指向创建对象的类,使用于创建新的对象的类共享原型对象的属性以及方法。 // 图片轮播类var LoopImages = function(imgArr, container) { this.imagesArray = imgArr; this.container = container;};LoopImages.prototype = { // 创建轮播图片 createImage: function() { console.log("LoopImages createImage function"); }, // 切换下一张图片 changeImage: function() { console.log("LoopImages changeImage function"); }};// 上下滑动切换类var SliderLoopImg = function(imgArr, container) { // 构造函数继承图片轮播类 LoopImages.call(this, imgArr, container);};SliderLoopImg.prototype = new LoopImages();// 重写继承的“切换下一张图片”方法SliderLoopImg.prototype.changeImage = function() { console.log("SliderLoopImg changeImage function");};单例模式:又称单体模式,是只允许实例化一次的对象类。 ...

June 21, 2019 · 4 min · jiezi

前端也要学Docker啊

Docker这两年非常火热,也是各大厂必用的好东西,这两天没事玩了一下感觉很不错,学起来也不难 写下此文共勉学习。 关于DockerDocker 可理解为跑在宿主机上的非常精简、小巧、高度浓缩的虚拟机。 它可以将容器里的进程安稳的在宿主机上运行。 Docker重要的三个概念必须要知道: Image: 镜像Container: 容器Repository: 镜像仓库为了好理解 我们从 Docker 的 Logo 入手: 图片是一条鲸鱼游在海里 身上载着N个集装箱,下面是Docker字样。OK 图片描述完毕 图片给出的信息: 海:宿主机集装箱:Docker容器鲸鱼+集装箱:Docker技术也就是说:Docker容器(集装箱)里可以存放着我们写的代码,然后 Docker 载着代码在大海(宿主机)里运行之所以用鲸鱼,可能是它在海里没什么天敌 体型又巨大而且游泳速度很快,毕竟Docker使用GO语言写的呢。 镜像(Image)、容器(Container)、仓库(Repository)上文中只说了Container,而Image与Container的关系 就像类与实例的关系: var p1 = new Person(); 即:p1是容器、Person是镜像。 至于仓库嘛 就相当于github的代码仓库,github是存代码的仓库,相应的 Docker 仓库就是存放镜像的。 只有理解上面的镜像(Image)、容器(Container)、仓库(Repository)才能破解下面的图: 上图分了三个块: Client(客户端 命令终端)DOCKER_HOST(Docker daemon)Resistry(镜像仓库)从左往右看,Client 中执行了几个命令,这些命令都与 Docker daemon(Docker的守护进程) 有交互,然后 Docker daemon 会根据相应命令做对应的动作。 docker build:表示创建了一个 Image,这是一条虚线 ,虚线从开始到结束指向了中间的Images框里。docker pull:表示从仓库中拉取 Image,就像 github 里 pull 代码一样。docker daemon 接收到 pull 指令,从 Registry(远程镜像仓库) 里找到对应镜像(这里是Nginx) 然后拉倒本地的 Images 中。docker run:向 daemon 发出运行指令,daemon 收到指令后去本地的 Images 中找对应镜像,如果能找到就会使用该镜像生成一个容器,如果没找到则会默认执行 docker pull 从仓库里下载,然后再生成容器,如果容器中运行着我们的代码,那么当容器运行后 代码也跟着 run 起来了Docker安装Docker分社区版(Community Edition,缩写为 CE)和企业版(Enterprise Edition,缩写为 EE)社区版是免费的,所以我们用CE版就可以了。Docker CE具体安装参考官网文档:CentOS、MacOS、Windows ...

June 15, 2019 · 1 min · jiezi

前端培训初级阶段场景实战20190613Nginx代理正确食用方式

前端最基础的就是 HTML+CSS+Javascript。掌握了这三门技术就算入门,但也仅仅是入门,现在前端开发的定义已经远远不止这些。前端小课堂(HTML/CSS/JS),本着提升技术水平,打牢基础知识的中心思想,我们开课啦(每周四)。 截止到 2019-05-30 期,所有成员都进行了一次分享。内部对课程进行了一些调整,之后会针对项目开始 review 。我这边预期准备进入中级阶段,中间还是会穿插一些实战。 前端培训目录 今天讲什么?nginx 的 servernginx 的 location 匹配规则nginx 的 root、rewrite、proxy_pass、aliasnginx 的命令以及报错日志今天为什么会开这个题目? 公司内部的前端构建工具升级(gulp),帮小伙伴处理了一下 nginx 的配置,辅助提升开发的体验。公司想要加快网页访问速度(前端缓存),为了测试,我改了我自己服务器的 nginx 配置。 PWA ()manifest ()其他方案(localStroage存)有老哥有科学有效的方案吗?缓存这块我还在实验中,我司有结果之后我会写个文章发出来。nginx 的 server定义虚拟主机相关。server 中通过 server_name 来匹配域名,listen来匹配端口 server_name用于匹配域名,需要已经映射的域名。举个栗子,我在阿里云有一台云服务器 IP:123.56.16.33:443。买了一个域名 lilnong.top。我现在把我的域名指向了我的ip。那所有请求我域名的都会到我这台服务器上。我需要用 server_name 来判断请求的是那台主机,再进行分发 listen用于匹配端口号,一般来说,我们当做服务的就需要加上 80 和 443 协议端口用途http80浏览器访问https443浏览器访问ftp21 server_name 与 host 匹配优先级完全匹配通配符在前的,如 *.lilnong.top在后的,如 www.lilnong.*正则匹配,如 ~^\.www\.lilnong\.com$如果都不匹配 优先选择 listen 配置项后有 default 或 default_server 的找到匹配 listen 端口的第一个 server 块nginx 的 location 匹配规则location 是什么?location 是用于在 server 服务中,根据 URL 进行匹配查找。属于 ngx_http_core_module 模块。 ...

June 10, 2019 · 2 min · jiezi

阿里云前端技术周刊第八期

作者:若欢校对:染陌 知乎:阿里云中台前端/全栈团队专栏Github:阿里云前端技术周刊 给我们投稿:传送门参与交流:传送门 前端速报 Angular 8 发布,想知道新版本都有哪些特性,快来戳我瞧瞧吧。TypeScript 3.5 版本发布,这不是一个大的版本迭代,但改进的性能,增量编译和Omit帮助类型,肯定会受到 TypeScript 用户的欢迎,不信就来试试看。趣前端 你用的那些CSS转场动画可以换一换了传统转场动画就是滑来滑去,淡入淡出这些。时代在召唤,技术在发展,可以试一试使用一些新的转场动画了。此文乃张鑫旭大神出品,必属精品,值得一读。 StylieStylie 是一个动画曲线代码导出工具,你只需要调整好曲线即可导出 CSS。 GB StudioGB Studio是适用于Mac,Linux和Windows的免费且易于使用的Game Boy复古冒险游戏creator。想了解如何从零开始制作自己的 GameBoy 游戏,这篇不容错过。 只使用 CSS 的实时聊天一个匪夷所思的实验,不使用 JS,只使用 CSS 实现实时聊天,如何实现的?快来瞅瞅吧。 编者推荐 webpack 5.0 新特性尝鲜虽然webpack5还没正式上线,但这并无妨碍我们对changelog上的新特性进行尝鲜实战。 Node.js 技术栈该文档是作者从事 Node.js Developer 以来的学习历程,旨在为大家提供一个较详细的学习教程,侧重点更倾向于 Node.js 服务端所涉及的技术栈,如果该文能使您得到帮助,不要吝啬你的小星星哦~ 五种使用 Array reduce 求平均数的方法数组作为 JavaScript 比较常用的数据类型之一,它的大部分方法都很容易理解和使用,但你真的把它们的功能使用到极致了吗?一起来看看如何借助 Array reduce 求平均数吧。 JavaScript 开发人员应该具备的技能在互联网技术如火如荼发展的今天,前端开发者很难知道应该要去关注些什么,如果你对此感到疑惑,不妨来看下CV Compiler的团队整理的2019 JavaScript的发展趋势报告。 厂内动态 Ant Design 4.0 进行时!Ant Design 于 17 年 12 月发布 3.0 以来,已经经历了 16 个月的时间。在此期间,我们修复了海量 Bug、以及增加大量新功能(更新日志)。我们也发布了 Ant Design Pro 4.0。支持了 TypeScript、区块以及对布局进行抽象。与此同时,我们也在思考下一步是什么,如何才能使 Ant Design 走的更远,我们预计在今年 Q4 发布 Ant Design 4.0 版本。以下是关于 4.0 的详细计划,当然这仍在计划中。正式发布时可能会有调整。 ...

June 9, 2019 · 1 min · jiezi

Webpack-4手工搭建深度分析

前言这是一篇关于webpack 4手工搭建重点问题的分析,webpack 3相关可以戳这里:https://github.com/Eleven90/webpack-pages-V3,这里并不试图从零手把手去堆代码,而是对其中的重点问题做稍微深入一点的解读。某些细节这里如果没有提及,项目代码里可能会有。项目地址:https://github.com/Eleven90/webpack-template为懒人准备的 webpack 配置模版,可以直接用于生产。这里单纯只做webpack打包的配置、前端工程化代码的组织等,有意抛开三大框架,从原始的H5出发去组织代码,关于React、Vue等配置并不复杂,可以在开发需要时添加。技术栈es6 + less + art-template + webpack 4普通 H5 开发中,引入组件化;引入 art-template 前端渲染引擎——目前前端模版里速度最快;配置 dev-server 调试模式,proxy 代理接口调试;配置 watch 模式,方便在生产环境中用 Charles 映射到本地文件;optimization 配置提取 runtime 代码;splitChunks 拆分代码,提取 vendor 主要缓存包,提取 common 次要缓存包;支持多页、多入口,自动扫描,可无限层级嵌套文件夹;MockJs 模拟 mock 数据;运行命令推荐使用yarn进行包管理,项目在某个时间节点被我切换到了yarn,但写文档时仍然是npm,对应变更一下即可。# 如果不熟悉或不想尝试yarn,直接npm install,然后npm run dev即可,不会有任何副作用yarn / yarn install # 安装全部依赖包yarn dev # 启动本地调试yarn dev-mock # 启动本地调试,MockJs模拟接口数据yarn dev:page-a # 启动本地调试,仅page-a页面yarn dev:page-b # 启动本地调试,仅page-b页面yarn build-dev # 打包代码,publicPath以/打头(可通过本地起服务访问build后的代码)yarn http-server # 启动http-server服务器,可用来访问yarn build-dev打包的代码yarn build-test # 打包测试环境代码yarn build # 打包生产环境代码# watch模式,移除了js、css的压缩,节省时间(watch时需要build压缩版代码,可自行修改)。yarn watch-dev # 启动watch模式,本地开发环境(通常用不上)yarn watch-test # 启动watch模式,测试环境yarn watch # 启动watch模式,生产环境# 如果你想用npm的话...(建议把yarn.lock文件也删掉)npm install # 安装全部依赖包npm run dev # 启动本地调试npm run dev-mock # 启动本地调试,MockJs模拟接口数据npm run dev:page-a # 启动本地调试,仅page-a页面npm run dev:page-b # 启动本地调试,仅page-b页面npm run build-dev # 打包代码,publicPath以/打头(可通过本地起服务访问build后的代码)npm run http-server # 启动http-server服务器,可用来访问npm run build-dev打包的代码npm run build-test # 打包测试环境代码npm run build # 打包生产环境代码# watch模式,移除了js、css的压缩,节省时间(watch时需要build压缩版代码,可自行修改)。npm run watch-dev # 启动watch模式,本地开发环境(通常用不上)npm run watch-test # 启动watch模式,测试环境npm run watch # 启动watch模式,生产环境Yarn和NPM的选择?通常使用NPM做包管理,但此项目使用Yarn,因为Yarn有:速度快、可离线安装、锁定版本、扁平化等更多优点。如果需要从npm切换到yarn,或者从yarn切换到npm时,请整体移除node_modules目录,及yarn.lock/package-lock.json文件,因yarn和npm两者的策略不同,导致相同的package.json列表安装后的node_modules结构是不同的(虽然这并不会引发bug,但建议在切换时清除后重新install)。Yarn常用命令yarn / yarn install // 安装全部(package.json)依赖包 —— npm installyarn run [dev] // 启动scripts命令yarn add [pkgName] // 安装依赖包(默认安装到dependencies下) —— npm install [pkgName]yarn add [pkgName]@[version] // 安装依赖包,指定版本 —— npm install [pkgName]@[version]yarn add [pkgName] -D // 安装依赖包,指定到devDependencies —— npm install [pkgName] -Dyarn remove [pkgName] // 卸载依赖包 —— npm uninstall [pkgName]yarn upgrade [pkgName] // 升级依赖包 —— npm update [pkgName]yarn upgrade [pkgName]@[version] // 升级依赖包,指定版本参考文档yarn中文网yarn安装 (预警:如果本机已经安装过NodeJS,使用brew安装yarn时,推荐使用brew install yarn --without-node命令,否则可能导致其它bug。)yarn命令yarn和npm命令对比Babel 转码这是最新的 babel 配置,和网上的诸多教程可能有不同,可以自行测试验证有效性。基础依赖包 ...

June 1, 2019 · 12 min · jiezi

前端脚手架构建实践

前面的话在前端工程化过程中,为了解决多项目中,相似度高的工作,便诞生许多前端脚手架,这里记录下自己实现一个简易前端脚手架过程的实践。主要是解决多个页面相似内容的复制粘贴问题,功能类似于Webstorm的Live template,或者Vscode的Snippets。 思路预先配置页面模板,预留关键字变量用户填写关键字变量,生成页面模板,输出到制定目录用到的包fs读写文件模块,这里主要用于读入用户配置文件,输出模板到文件 commanderNodeJs命令行工具,提供了用户命令行输入和参数解析,用户解析用户输入 inquirerNodeJs交互式命令行工具,询问操作者问题,获取用户输入,校验回答的合法性 metalsmith文件处理,读写操作 handlebars将模板中的变量替换为用户输入,编译模板,类似框架如:artTemplate,Jade pathNodeJs的路径操作库,如合并路径 chalk命令行输出样式美化 具体实现首先在一个新的文件夹,如xxx-tools下 npm init 创建一个node项目,因为是要做成一个npm包的脚手架,所以在包的取名上一定要唯一,即package.json中name字段,避免在发包的时候和网上已经存在的npm包重名,报403没有权限的错。在xxx-tools文件夹下创建bin文件夹,同时在bin文件夹下创建脚本tempTool文件,内容如下:#!/usr/bin/env nodeconsole.log('Hello World');注意哦,#!/usr/bin/env node 这个是Linux规范,用来指明了这个执行脚本的解释程序,要是没有这一行,默认用当前Shell去解释这个脚本 在package.json中增加bin配置: "bin": { "tempTool": "./bin/tempTool" },到目前为止,一个简单的前端脚手架实现了,在npm官网注册,在项目里执行npm login登录,之后npm publish如果一切顺利,npm包提交完毕,可以在其它项目中执行npm i -g xxx-tools,安装这个包,执行xxx-tools命令,输出 Hello World,脚手架开发过程中,也涉及到在本地调试,可以直执行node ./bin/xxx-tools现在来加入具体的开发流程,用户的输入,输入信息的读取等等,bin文件修改如下#!/usr/bin/env nodeconst program = require('commander');const chalk = require('chalk');const { loadTemplate } = require('../src/lib/writeTemp');const log = data => console.log(chalk.green(data));log('初始化模板配置');program .command('create') .description('create template') .option('-d') .action(async function () { const result = await loadTemplate(); result ? null : log('配置完毕'); });program.parse(process.argv);用户执行create命令,在这里调用了loadTemplate函数,看一下这个函数 ...

May 29, 2019 · 2 min · jiezi

面试必备webpack-中那些最易混淆的-5-个知识点

面试必备!webpack 中那些易混淆的 5 个知识点前两天为了优化公司的代码打包项目,恶补了很多 webpack4 的知识。要是放在几年前让我学习 webpack 我肯定是拒绝的,之前看过 webpack 的旧文档,比我们内部项目的文档还要简陋。 但是最近看了一下 webpack4 的文档,发现 webpack官网的 指南 写的还不错,跟着这份指南学会 webpack4 基础配置完全不是问题,想系统学习 webpack 的朋友可以看一下。 今天我主要分享的是一些 webpack 中的易混淆知识点,也是面试的常见内容。我把这些分散在文档和教程里的内容总结起来,目前看是全网独一份,大家可以加个收藏,方便以后检索和学习。 <br/> 友情提示:本文章不是入门教程,不会费大量笔墨去描写 webpack 的基础配置,请读者配合教程源代码食用。<br/> 1.webpack 中,module,chunk 和 bundle 的区别是什么?说实话我刚开始看 webpack 文档的时候,对这 3 个名词云里雾里的,感觉他们都在说打包文件,但是一会儿 chunk 一会儿 bundle 的,逐渐就迷失在细节里了,所以我们要跳出来,从宏观的角度来看这几个名词。 webpack 官网对 chunk 和 bundle 做出了解释,说实话太抽象了,我这里举个例子,给大家形象化的解释一下。 首先我们在 src 目录下写我们的业务代码,引入 index.js、utils.js、common.js 和 index.css 这 4 个文件,目录结构如下: src/├── index.css├── index.html # 这个是 HTML 模板代码├── index.js├── common.js└── utils.jsindex.css 写一点儿简单的样式: ...

May 29, 2019 · 5 min · jiezi

????如何快速开发一个自己的项目脚手架

引言下面是一个使用脚手架来初始化项目的典型例子。 随着前端工程化的理念不断深入,越来越多的人选择使用脚手架来从零到一搭建自己的项目。其中大家最熟悉的就是create-react-app和vue-cli,它们可以帮助我们初始化配置、生成项目结构、自动安装依赖,最后我们一行指令即可运行项目开始开发,或者进行项目构建(build)。 这些脚手架提供的都是普遍意义上的最佳实践,但是我在开发中发现,随着业务的不断发展,必然会出现需要针对业务开发的实际情况来进行调整。例如: 通过调整插件与配置实现 Webpack 打包性能优化后删除脚手架构建出来的部分功能项目架构调整融合公司开发工具……总而言之,随着业务发展,我们往往会沉淀出一套更“个性化”的业务方案。这时候我们最直接的做法就是开发出一个该方案的脚手架来,以便今后能复用这些最佳实践与方案。 1. 脚手架怎么工作?功能丰富程度不同的脚手架,复杂程度自然也不太一样。但是总体来说,脚手架的工作大体都会包含几个步骤: 初始化,一般在这个时候会进行环境的初始化,做一些前置的检查用户输入,例如用 vue-cli 的时候,它会“问”你很多配置选项生成配置文件生成项目结构,这是候可能会使用一个项目模版安装依赖清理、校验等收尾工作此外,你还需要处理命令行行为等。往往我们只是想轻量级、快速得创建一个特定场景的脚手架(不用想vue-cli那么完备)。而对于想要快速创建一个脚手架,其实我们不用完全从零开始。Yeoman 就是一个可以帮我们快速创建脚手架的工具。 可能很多同学都不太了解,那么先简单介绍一下 Yeoman 是什么,又是如何帮我们来简化脚手架搭建的。 首先,Yeoman 可以简单理解为是一个脚手架的运行框架,它定义了一个脚手架在运行过程中所要经历的各个阶段(例如我们上面说的,可能会先读取用户输入,然后生成项目文件,最后安装依赖),我们所需要的就是在生命周期的对应阶段,填充对应的操作代码即可。而我们填充代码的地方,在 Yeoman 中叫做 generator,物如其名,Yeoman 通过调用某个 generator 即可生成(generate)对应的项目。 如果你还不是特别清楚它们之间的关系,那么可以举个小例子: 将脚手架开发类比为前端组件开发,Yeoman 的角色就像是 React,是一个框架,尤其是定义了组件的生命周期函数;而 generator 类似于你写的一个 React 业务组件,根据 React 的规则在各个生命周期中填代码即可。 Yeoman 内置的“生命周期”方法执行顺序如下: initializingpromptingdefaultwritingconflictsinstallend其中 default 阶段会执行你自定义地各种方法。 同时,Yeoman 还集成了脚手架开发中常用的各类工具,像是文件操作、模版填充、终端上的用户交互功能,命令行等,并且封装成了简单易用的方法。 通过这两点,Yeoman 可以帮我们大大规范与简化脚手架的开发。 2. 开发一个自己的脚手架了解了一些脚手架的工作方式与 Yeoman 的基本概念,咱们就可以来创建一个属于自己的脚手架。作为例子,这个脚手架的功能很简单,它会为我们创建一个最简版的基于 Webpack 的前端项目。最终脚手架使用效果如下: 2.1. 准备一个项目模版脚手架是帮助我们快速生成一套既定的项目架构、文件、配置,而最常见的做法的就是先写好一套项目框架模版,等到脚手架要生成项目时,则将这套模版拷贝到目标目录下。这里其实会有两个小点需要关注。 第一个是模版内变量的填充。 在模版中的某些文件内容可能会需要生成时动态替换,例如根据用户在终端中输入的内容,动态填充package.json中的name值。而 Yeoman 内置了 ejs 作为模版引擎,可以直接使用。 第二个就是模版的放置位置。 一种是直接放在本地,也就是直接放到 generator 中,跟随 generator 一起下载,每次安装都是本地拷贝,速度很快,但是项目模版自身的更新升级比较困难,需要提示用户升级 generator。 ...

May 18, 2019 · 3 min · jiezi

Vue开发总结-及-一些最佳实践-已更新

基本开发环境vue-cli3 创建的项目,vscode 作为代码编写工具vscode插件推荐:vscode 插件配置 文章目录项目目录结构介绍UI 框架选择main,js 处理axios 请求二次封装1. 项目目录结构简介├── public // index.html├── src // 业务相关│ ├── assets // 静态文件(css, images)│ ├── components // 全局组件│ ├── layouts // 基础样式布局│ ├── plugin // 样式及工具引入│ ├── request // 请求配置│ ├── router // 路由│ ├── store // 全局状态管理│ ├── utils // 工具文件│ ├── app.vue // 入口文件│ ├── main.js // 主要配置文件│ └── views // 页面├── .eslintrc.js // eslint 检查配置├── .env.release // 测试环境变量├── .env.pre-build // 预发环境变量└── vue.config.js // webpack 打包配置2. UI 框架选择经框架选择验证对比 element,iview,ant-design-vue最终选择 ant-design-vue,传送门 https://vue.ant.design/docs/vue/introduce-cn/ ...

May 15, 2019 · 3 min · jiezi

前端开发上线规范

开发个人项目时,不用准守所谓的开发上线规范,随意点也无所谓。而在开发公司项目时,我们不得不为设立一套规则和流程来进行规范。其目的有三个: 用户能够正常访问团队能够协同开发个人能够成长进步为此,我们团队在逐步的去探索一套,适合我们的前端开发上线流程。探索的过程从两方面入手。 工程化流程化工程化我们团队的 gitlab 仓库中已经存在近 200 项目,如果各自为政,没有文档、使用不同技术栈、代码风格也不进行代码校验。那么也就只有写这个项目的人才能进行维护了,换一个人肯定是一脸懵逼。为此我们团队在诸多的技术栈中,选择以 react、react-native 和小程序技术为核心,进行项目开发。 进一步地,使用工程化的手段,打造 react、react-native 和小程序的种子工程。新建项目时,所有的项目都是以种子工程为模板,在此之上进行开发。种子工程内,集成各种团队内事先约定的库和工具,这样就统一了团队的使用和核心库和工具。 react、react-native 数据管理:✅ reduxmobxgraphQLjs 代码检查:(Formatting & Code-quality Rules) ✅eslint ✅ standard 规范(简单) https://standardjs.com/readme...airbnb 规范 (严格)jslint✅ html/css/js/md ... 代码风格:prettier(Only Formatting Rules)git hooks ✅ husky "pre-commit": "pretty-quick --staged && npx standard --fix".git/hooks/pre-commit✅ 编辑器插件(以 VScode 为例) prettier "editor.formatOnSave": true,"prettier.disableLanguages": ["javascript"] // 暂时没有研究透在 JS 这块 prettier 比 eslint 优势所在,因此只对 html/css 等做 Formatting,JS Formatting 依旧用的是 eslint standard 规范vscode-standardjs "standard.autoFixOnSave": true,es7-react-js-snippets 代码输入提示示例 WubaRN 种子工程 ...

May 10, 2019 · 1 min · jiezi

《阿里云前端技术周刊》第一期

作者:染陌写在前面这是一个前端技术蓬勃发展的时代,从小程序到 PWA,从 Node.js 到三大框架,各种技术层出不穷。各种新技术需要我们怀揣着好奇心去探索学习,开阔自己的技术视野以及技术广度。然而互联网上的资源多而不精,本着将更多优质的资源整合学习的目的,《阿里云前端技术周刊》诞生了!《阿里云前端技术周刊》由阿里云智能商业中台体验技术团队整理编写,希望能给大家带来更多有价值的内容,共同学习,共同进步!欢迎大家关注我们,后续会为大家提供更多高质量的内容输出。知乎: 阿里云中台前端/全栈团队专栏Github:阿里云前端技术周刊给我们投稿->传送门前端速报Chrome75 将原生支持图片以及iframe实现通过loading属性进行懒加载。更多<img loading=“lazy” /><iframe loading=“lazy”></iframe>微软发布基于 Chromium 的预览版 Edge。 更多jQuery 3.4.0 版本发布。更多JS 引擎 V8 发布 v7.4 性能再次大幅提高。更多编者推荐《24 个实例入门并掌握「Webpack4」》文章由一系列 Webpack 的实例为读者讲解 Webpack4 的使用,将各个技术点拆开剖析讲解,有利于读者快速学习理解 Webpack4 的基本使用。《Serverless 给前端带来了什么》一篇文章带领大家看看 Serverless 能给前端带来怎么样的价值与能力。《逐行分析Koa v1 中间件原理》一篇非常精细的剖析 koa 中间件原理的文章,逐行级别的代码分析,带领读者深入 koa 中间件。《前端开发人员手册》一本非常全面的前端开发人员手册,从各种知识点到资源建议,内容非常丰富,值得一看。趣前端OS.js一款用 JavaScript 编写的运行于浏览器上的桌面操作系统。Octotree一款为 GitHub 提供一份更直观展示目录结构的插件,妈妈再也不用担心我在 Web 端看代码啦~关于我们我们是阿里云中台前端团队。详情如有兴趣加入我们,简历请发至: ranmo.cy@alibaba-inc.com

April 20, 2019 · 1 min · jiezi

webpack学习记录之可能面试思考题

本文记录一些webpack的知识点及面试过程中可能会出现的面试题或思考题?source map 基本使用与其原理是什么entry,output中常用的配置项有哪些,如何给静态资源配置CDN上环境external的使用tree shaking

April 20, 2019 · 1 min · jiezi

前端工程师必备:前端的模块化

JS模块化模块化的理解什么是模块?将一个复杂的程序依据一定的规则(规范)封装成几个块(文件), 并进行组合在一起;块的内部数据/实现是私有的, 只是向外部暴露一些接口(方法)与外部其它模块通信;一个模块的组成数据—>内部的属性;操作数据的行为—>内部的函数;模块化是指解决一个复杂的问题时自顶向下把系统划分成若干模块的过程,有多种属性,分别反映其内部特性;模块化编码:编码时是按照模块一个一个编码的, 整个项目就是一个模块化的项目;非模块化的问题页面加载多个js的问题:<script type=“text/javascript” src=“module1.js”></script><script type=“text/javascript” src=“module2.js”></script><script type=“text/javascript” src=“module3.js”></script><script type=“text/javascript” src=“module4.js”></script>发生问题:难以维护 ;依赖模糊;请求过多;所以,这些问题可以通过现代模块化编码和项目构建来解决;模块化的优点更好地分离:避免一个页面中放置多个script标签,而只需加载一个需要的整体模块即可,这样对于HTML和JavaScript分离很有好处;更好的代码组织方式:有利于后期更好的维护代码;按需加载:提高使用性能,和下载速度,按需求加载需要的模块避免命名冲突:JavaScript本身是没有命名空间,经常会有命名冲突,模块化就能使模块内的任何形式的命名都不会再和其他模块有冲突。更好的依赖处理:使用模块化,只需要在模块内部申明好依赖的就行,增加删除都直接修改模块即可,在调用的时候也不用管该模块依赖了哪些其他模块。模块化的发展历程原始写法只是把不同的函数简单地放在一起,就算一个模块;function fun1(){ //…}function fun2(){ //…}//上面的函数fun1,fun2组成了一个模块,使用的时候直接调用某个函数就行了。缺点:“污染"了全局变量,无法保证不与其他模块发生变量名冲突;模块成员之间看不出直接关系。对象写法为了解决污染全局变量的问题,可以把模块写成一个对象,所有的模块成员都放到这个对象里面。 var module1 = new Object({ count : 0, fun1 : function (){ //… }, fun2 : function (){ //… } }); //这个里面的fun1和fun2都封装在一个赌侠宁里,可以通过对象.方法的形式进行调用; module1.fun1();优点:减少了全局上的变量数目;缺点:本质是对象,而这个对象会暴露所有模块成员,内部状态可以被外部改写。立即执行函数(IIFE模式)避免暴露私有成员,所以使用立即执行函数(自调函数,IIFE);作用: 数据是私有的, 外部只能通过暴露的方法操作var module1 = (function(){ var count = 0; var fun1 = function(){ //… } var fun2 = function(){ //… } //将想要暴露的内容放置到一个对象中,通过return返回到全局作用域。 return{ fun1:fun1, fun2:fun2 }})()//这样的话只能在全局作用域中读到fun1和fun2,但是读不到变量count,也修改不了了。//问题:当前这个模块依赖另一个模块怎么办?IIFE的增强(引入依赖)如果一个模块很大,必须分成几个部分,或者一个模块需要继承另一个模块,这时就有必要采用"增强模式”;IIFE模式增强:引入依赖;这就是现代模块实现的基石;var module1 = (function (mod){ mod.fun3 = function () { //… }; return mod;})(module1);//为module1模块添加了一个新方法fun3(),然后返回新的module1模块。//引入jquery到项目中;var Module = (function($){ var $body = $(“body”); // we can use jQuery now! var foo = function(){ console.log($body); // 特权方法 } // Revelation Pattern return { foo: foo }})(jQuery)Module.foo();js模块化需要解决那些问题:1.如何安全的包装一个模块的代码?(不污染模块外的任何代码)2.如何唯一标识一个模块?3.如何优雅的把模块的API暴漏出去?(不能增加全局变量)4.如何方便的使用所依赖的模块?模块化规范Node: 服务器端Browserify : 浏览器端CommonJS:服务器端概述Node 应用由模块组成,采用 CommonJS 模块规范。CommonJS规范规定,每个模块内部,module变量代表当前模块。这个变量是一个对象,它的exports属性(即module.exports)是对外的接口。加载某个模块,其实是加载该模块的module.exports属性。特点所有代码都运行在模块作用域,不会污染全局作用域。模块可以多次加载,但是只会在第一次加载时运行一次,然后运行结果就被缓存了,以后再加载,就直接读取缓存结果。要想让模块再次运行,必须清除缓存。模块加载的顺序,按照其在代码中出现的顺序。基本语法:定义暴露模块 : exportsexports.xxx = value// 通过module.exports指定暴露的对象valuemodule.exports = value引入模块 : requirevar module = require(‘模块相对路径’)引入模块发生在什么时候?Node:运行时, 动态同步引入;Browserify:在运行前对模块进行编译/转译/打包的处理(已经将依赖的模块包含进来了), 运行的是打包生成的js, 运行时不需要再从远程引入依赖模块;CommonJS通用的模块规范(同步)Node内部提供一个Module构建函数。所有模块都是Module的实例。每个模块内部,都有一个module对象,代表当前模块。module.exports属性表示当前模块对外输出的接口,其他文件加载该模块,实际上就是读取module.exports变量。Node为每个模块提供一个exports变量,指向module.exports。如果一个模块的对外接口,就是一个单一的值,不能使用exports输出,只能使用module.exports输出。Modules/1.0规范包含内容:模块的标识应遵循的规则(书写规范)定义全局函数require,通过传入模块标识来引入其他模块,执行的结果即为模块暴露出来的API;如果被require函数引入的模块中也包含依赖,那么依次加载这些依赖;如果引入模块失败,那么require函数应该报一个异常;模块通过变量exports来向外暴露API,exports赋值暴露的只能是一个对象exports = {Obj},暴露的API须作为此对象的属性。exports本质是引入了module.exports的对象。不能直接将exports变量指向一个值,因为这样等于切断了exports与module.exports的联系。如果暴露的不是变量exports,而是module.exports。module变量代表当前模块,这个变量是一个对象,它的exports属性(即module.exports)是对外的接口。加载某个模块,其实是加载该模块的module.exports属性。exports=module.exports={Obj}node中的commonJS教程1.安装node.js;2.创建项目结构//结构如下|-modules |-module1.js//待引入模块1 |-module2.js//待引入模块2 |-module3.js//待引入模块3|-app.js//主模块|-package.json { “name”: “commonjsnode”, “version”: “1.0.0” }3.下载第三方模块:举例expressnpm i express –save4.模块化编码// module1 // 使用module.exports = value向外暴露一个对象module.exports = { name: ’this is module1’, foo(){ console.log(‘module1 foo()’); }}// module2 // 使用module.exports = value向外暴露一个函数 module.exports = function () { console.log(‘module2()’);}// module3 // 使用exports.xxx = value向外暴露一个对象 exports.foo = function () { console.log(‘module3 foo()’); }; exports.bar = function () { console.log(‘module3 bar()’); }; exports.name = ’this is module3’//app.js文件var uniq = require(‘uniq’);//引用模块let module1 = require(’./modules/module1’);let module2 = require(’./modules/module2’);let module3 = require(’./modules/module3’);//使用模块module1.foo();module2();module3.foo();module3.bar();module3.name;5.通过node运行app.js命令:node.app.js工具:右键–>运行浏览器中的commonJS教程借助Browserify步骤创建项目结构|-js |-dist //打包生成文件的目录 |-src //源码所在的目录 |-module1.js |-module2.js |-module3.js |-app.js //应用主源文件|-index.html //浏览器上的页面|-package.json { “name”: “browserify-test”, “version”: “1.0.0” }下载browserify全局: npm install browserify -g局部: npm install browserify –save-dev定义模块代码:index.html文件要运行在浏览器上,需要借助browserify将app.js文件打包编译,如果直接在index.html引入app.js就会报错。打包处理js:根目录下运行browserify js/src/app.js -o js/dist/bundle.js页面使用引入:<script type=“text/javascript” src=“js/dist/bundle.js”></script> AMD : 浏览器端CommonJS规范加载模块是同步的,也就是说,只有加载完成,才能执行后面的操作。AMD规范则是非同步加载模块,允许指定回调函数,可以实现异步加载依赖模块,并且会提前加载;由于Node.js主要用于服务器编程,模块文件一般都已经存在于本地硬盘,所以加载起来比较快,不用考虑非同步加载的方式,所以CommonJS规范比较适用。如果是浏览器环境,要从服务器端加载模块,这时就必须采用非同步模式,因此浏览器端一般采用AMD规范。语法AMD规范基本语法定义暴露模块: define([依赖模块名], function(){return 模块对象})引入模块: require([‘模块1’, ‘模块2’, ‘模块3’], function(m1, m2){//使用模块对象})兼容CommonJS规范的输出模块define(function (require, exports, module) { var reqModule = require("./someModule"); requModule.test(); exports.asplode = function () { //someing }}); AMD:异步模块定义规范(预加载)AMD规范:https://github.com/amdjs/amdj…AMD是"Asynchronous Module Definition"的缩写,意思就是"异步模块定义"。它采用异步方式加载模块,模块的加载不影响它后面语句的运行。所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行。AMD也采用require()语句加载模块,但是不同于CommonJS,它要求两个参数:require([module], callback);第一个参数[module],是一个数组,里面的成员就是要加载的模块;第二个参数callback,则是加载成功之后的回调函数。目前,主要有两个Javascript库实现了AMD规范:RequireJS和curl.js。RequireJS优点实现js文件的异步加载,避免网页失去响应;管理模块之间的依赖性,便于代码的编写和维护。require.js使用教程下载require.js, 并引入官网: https://requirejs.org/中文:https://blog.csdn.net/sanxian…github : https://github.com/requirejs/…将require.js导入项目: js/libs/require.js创建项目结构|-js |-libs |-require.js // 引入的require.js |-modules |-alerter.js |-dataService.js |-main.js|-index.html定义require.js的模块代码require.js加载的模块,采用AMD规范。也就是说,模块必须按照AMD的规定来写。具体来说,就是模块必须采用特定的define()函数来定义;如果一个模块不依赖其他模块,那么可以直接定义在define()函数之中。define([‘myLib’], function(myLib){ function foo(){ myLib.doSomething(); } // 暴露模块 return {foo : foo};});//当require()函数加载上面这个模块的时候,就会先加载myLib.js文件。 - 如果这个模块还依赖其他模块,那么define()函数的第一个参数,必须是一个数组,指明该模块的依赖性; // dataService.js define(function () { let msg = 'this is dataService' function getMsg() { return msg.toUpperCase() } return {getMsg} }) // alerter.js define(['dataService', 'jquery'], function (dataService, $) { let name = 'Tom2' function showMsg() { $('body').css('background', 'gray') alert(dataService.getMsg() + ', ' + name) } return {showMsg} }) 应用主(入口)js: main.js使用require.config()方法,我们可以对模块的加载行为进行自定义。require.config()就写在主模块main.js的头部,参数就是一个对象,这个对象的paths属性指定各个模块的加载路径。 (function () { //配置 require.config({ //基本路径 baseUrl: “js/”, //模块标识名与模块路径映射 paths: { “alerter”: “modules/alerter”,//此处不能写成alerter.js,会报错 “dataService”: “modules/dataService”, } }) //引入使用模块 require( [‘alerter’], function(alerter) { alerter.showMsg() }) })()页面使用模块:<script data-main=“js/main” src=“js/libs/require.js”></script>定义模块require.config()接受一个配置对象,这个对象除了有前面说过的paths属性之外,还有一个shim属性,专门用来配置不兼容的模块。具体来说,每个模块要定义:1、exports值(输出的变量名),表明这个模块外部调用时的名称;2、deps数组,表明该模块的依赖性。支持的配置项:baseUrl :所有模块的查找根路径。当加载纯.js文件(依赖字串以/开头,或者以.js结尾,或者含有协议),不会使用baseUrl。如未显式设置baseUrl,则默认值是加载require.js的HTML所处的位置。如果用了data-main属性,则该路径就变成baseUrl。baseUrl可跟require.js页面处于不同的域下,RequireJS脚本的加载是跨域的。唯一的限制是使用text! plugins加载文本内容时,这些路径应跟页面同域,至少在开发时应这样。优化工具会将text! plugin资源内联,因此在使用优化工具之后你可以使用跨域引用text! plugin资源的那些资源。paths:path映射那些不直接放置于baseUrl下的模块名。设置path时起始位置是相对于baseUrl的,除非该path设置以"/“开头或含有URL协议(如http:)。用于模块名的path不应含有.js后缀,因为一个path有可能映射到一个目录。路径解析机制会自动在映射模块名到path时添加上.js后缀。在文本模版之类的场景中使用require.toUrl()时它也会添加合适的后缀。在浏览器中运行时,可指定路径的备选(fallbacks),以实现诸如首先指定了从CDN中加载,一旦CDN加载失败则从本地位置中加载这类的机制;shim: 为那些没有使用define()来声明依赖关系、设置模块的"浏览器全局变量注入"型脚本做依赖和导出配置。使用第三方基于require.js的框架(jquery)将jquery的库文件导入到项目: js/libs/jquery-1.10.1.js在main.js中配置jquery路径paths: { ‘jquery’: ’libs/jquery-1.10.1’}在alerter.js中使用jquerydefine([‘dataService’, ‘jquery’], function (dataService, $) { var name = ‘xfzhang’ function showMsg() { $(‘body’).css({background : ‘red’}) alert(name + ’ ‘+dataService.getMsg()) } return {showMsg} })使用第三方不基于require.js的框架(angular)将angular.js导入项目:js/libs/angular.js流行的函数库(比如jQuery)符合AMD规范,更多的库并不符合。这样的模块在用require()加载之前,要先用require.config()方法,定义它们的一些特征。// main.js中配置(function () { //配置 require.config({ //基本路径 baseUrl: “js/”, //模块标识名与模块路径映射 paths: { //第三方库作为模块 ‘jquery’ : ‘./libs/jquery-1.10.1’, ‘angular’ : ‘./libs/angular’, //自定义模块 “alerter”: “./modules/alerter”, “dataService”: “./modules/dataService” }, /* 配置不兼容AMD的模块 exports : 指定与相对应的模块名对应的模块对象 */ shim: { ‘angular’ : { exports : ‘angular’ } } }) //引入使用模块 require( [‘alerter’, ‘angular’], function(alerter, angular) { alerter.showMsg() console.log(angular); })})()CMD : 浏览器端CMD规范:https://github.com/seajs/seaj…CMD规范专门用于浏览器端,模块的加载是异步的,模块使用时才会加载执行。CMD规范整合了CommonJS和AMD规范的特点。在 Sea.js 中,所有 JavaScript 模块都遵循 CMD模块定义规范基本语法定义暴露模块:// 没有依赖的模块define(function(require, module, exports){ let value = ‘xxx’; //通过require引入依赖模块 //通过module.exports/exports来暴露模块 exports.xxx = value module.exports = value})// 有依赖的模块define(function(require, exports, module){ //引入依赖模块(同步) var module2 = require(’./module2’) //引入依赖模块(异步) require.async(’./module3’, function (m3) { …… }) //暴露模块 exports.xxx = value})使用模块seajs.use([‘模块1’, ‘模块2’])sea.js简单使用教程下载sea.js, 并引入官网: http://seajs.org/github : https://github.com/seajs/seajs将sea.js导入项目: js/libs/sea.js如何定义导出模块 :define()exportsmodule.exports如何依赖模块:require()如何使用模块: seajs.use()创建项目结构|-js |-libs |-sea.js |-modules |-module1.js |-module2.js |-module3.js |-module4.js |-main.js|-index.html定义sea.js的模块代码module1.jsdefine(function (require, exports, module) { //内部变量数据 var data = ’this is module1’ //内部函数 function show() { console.log(‘module1 show() ’ + data) } //向外暴露 exports.show = show})module2.jsdefine(function (require, exports, module) { module.exports = { msg: ‘I Will Back’ }})module3.jsdefine(function (require, exports, module) { const API_KEY = ‘abc123’ exports.API_KEY = API_KEY})module4.jsdefine(function (require, exports, module) { //引入依赖模块(同步) var module2 = require(’./module2’); function show() { console.log(‘module4 show() ’ + module2.msg) } exports.show = show //引入依赖模块(异步) require.async(’./module3’, function (m3) { console.log(‘异步引入依赖模块3 ’ + m3.API_KEY) })})main.js : 主(入口)模块define(function (require) { var m1 = require(’./module1’) var m4 = require(’./module4’) m1.show() m4.show()})index.html:<script type=“text/javascript” src=“js/libs/sea.js”></script><script type=“text/javascript”> seajs.use(’./js/modules/main’)</script>ES6模块化模块化的规范:CommonJS和AMD两种。前者用于服务器,后者用于浏览器。而ES6 中提供了简单的模块系统,完全可以取代现有的CommonJS和AMD规范,成为浏览器和服务器通用的模块解决方案。ES6 模块的设计思想,是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。CommonJS 和 AMD 模块,都只能在运行时确定这些东西。基本用法es6 中新增了两个命令 export 和 import ;export 命令用于规定模块的对外接口;import 命令用于输入其他模块提供的功能。一个模块就是一个独立的文件。该文件内部的所有变量,外部无法获取。如果你希望外部能够读取模块内部的某个变量,就必须使用export关键字输出该变量。下面是一个JS文件,里面使用export命令输出变量。// math.jsexport const add = function (a, b) { return a + b}export const subtract = function (a, b) { return a - b}使用export命令定义了模块的对外接口以后,其他JS文件就可以通过import命令加载这个模块(文件)。// main.jsimport { add, subtract } from ‘./math.js’add(1, 2)substract(3, 2)定义暴露模块 : export暴露一个对象:默认暴露,暴露任意数据类型,暴露什么数据类型,接收什么数据类型export default 对象暴露多个:常规暴露,暴露的本质是对象,接收的时候只能以对象的解构赋值的方式来接收值export var xxx = value1export let yyy = value2// 暴露一个对象var xxx = value1let yyy = value2export {xxx, yyy}引入使用模块 : importdefault模块:import xxx from ‘模块路径/模块名’其它模块import {xxx, yyy} from ‘模块路径/模块名’import * as module1 from ‘模块路径/模块名’export 详细用法export不止可以导出函数,还可以导出,对象、类、字符串等等;暴露多个:分别暴露export const obj = {test1: ‘’}export const test = ‘’export class Test { constuctor() { }}// 或者,直接在暴露的地方定义导出函数或者变量export let foo = ()=>{console.log(‘fnFoo’);return “foo”},bar=“stringBar"一起暴露,推荐使用这种写法,这样可以写在脚本尾部,一眼就看清楚输出了哪些变量。let a=1let b=2let c=3export { a,b,c }还可以通过as改变输出名称// test.jslet a = 1let b = 2let c = 3export { a as test, b, c};import { test, b, c } from ‘./test.js’ // 改变命名后只能写 as 后的命名通过通配符暴露其他引入的模块// test.jslet a = 1let b = 2let c = 3export { a as test, b, c};// lib.js引入test.js的内容export * from ‘./test.js’// 引入import {test,b,c} from ‘./lib.js’暴露一个对象,默认暴露export default指定默认输出,import无需知道变量名就可以直接使用// test.jsexport default function () { console.log(‘hello world’)}//引入import say from ‘./test.js’ // 这里可以指定任意变量名say() // hello world常用的模块import $ from ‘jQuery’ // 加载jQuery 库import _ from ’lodash’ // 加载 lodashimport moment from ‘moment’ // 加载 momentimport详细用法import 为加载模块的命令,基础使用方式和之前一样// main.jsimport { add, subtract } from ‘./test’// 对于export default 导出的import say from ‘./test’通过 as 命令修改导入的变量名import {add as sum, subtract} from ‘./test’sum (1, 2)加载模块的全部,除了指定输出变量名或者 export.default 定义的导入, 还可以通过 * 号加载模块的全部。// math.jsexport const add = function (a, b) { return a + b}export const subtract = function (a, b) { return a - b}//引入import * as math from ‘./test.js’math.add(1, 2)math.subtract(1, 2)ES6-Babel-Browserify使用教程问题: 所有浏览器还不能直接识别ES6模块化的语法解决:使用Babel将ES6—>ES5(使用了CommonJS) —-浏览器还不能直接执行;使用Browserify—>打包处理js—-浏览器可以运行定义package.json文件{ “name” : “es6-babel-browserify”, “version” : “1.0.0”}安装babel-cli, babel-preset-es2015和browserifynpm install babel-cli browserify -gnpm install babel-preset-es2015 –save-dev 定义.babelrc文件,这是一个babel的设置文件{ “presets”: [“es2015”] }编码// js/src/module1.jsexport function foo() { console.log(‘module1 foo()’);};export let bar = function () { console.log(‘module1 bar()’);};export const DATA_ARR = [1, 3, 5, 1];// js/src/module2.jslet data = ‘module2 data’; function fun1() { console.log(‘module2 fun1() ’ + data);};function fun2() { console.log(‘module2 fun2() ’ + data);};export {fun1, fun2};// js/src/module3.jsexport default { name: ‘Tom’, setName: function (name) { this.name = name }}// js/src/app.jsimport {foo, bar} from ‘./module1’import {DATA_ARR} from ‘./module1’import {fun1, fun2} from ‘./module2’import person from ‘./module3’import $ from ‘jquery’//引入完毕$(‘body’).css(‘background’, ‘red’)foo()bar()console.log(DATA_ARR);fun1()fun2()person.setName(‘JACK’)console.log(person.name);编译使用Babel将ES6编译为ES5代码(但包含CommonJS语法) : babel js/src -d js/lib使用Browserify编译js : browserify js/lib/app.js -o js/lib/bundle.js页面中引入测试<script type=“text/javascript” src=“js/lib/bundle.js”></script>引入第三方模块(jQuery)1). 下载jQuery模块:npm install jquery@1 –save- 2). 在app.js中引入并使用import $ from 'jquery'$('body').css('background', 'red')总结模块化方案优点缺点commonJS复用性强; 使用简单;实现简单;有不少可以拿来即用的模块,生态不错;同步加载不适合浏览器,浏览器的请求都是异步加载;不能并行加载多个模块。AMD异步加载适合浏览器可并行加载多个模块;模块定义方式不优雅,不符合标准模块化ES6可静态分析,提前编译面向未来的标准;浏览器原生兼容性差,所以一般都编译成ES5;目前可以拿来即用的模块少,生态差AMD和CMD区别:权威参考:https://github.com/seajs/seaj…对于依赖的模块,AMD 是提前执行,CMD 是延迟执行。不过 RequireJS 从 2.0 开始,也改成可以延迟执行(根据写法不同,处理方式不同)。CMD 推崇 as lazy as possible.CMD 推崇依赖就近,AMD 推崇依赖前置。// CMDdefine(function(require, exports, module) { var a = require(’./a’); a.doSomething() // 此处略去 100 行 var b = require(’./b’) // 依赖可以就近书写 b.doSomething() // … })// AMD 默认推荐的是define([’./a’, ‘./b’], function(a, b) { // 依赖必须一开始就写好 a.doSomething() // 此处略去 100 行 b.doSomething() …})虽然 AMD 也支持 CMD 的写法,同时还支持将 require 作为依赖项传递,但 RequireJS 的作者默认是最喜欢上面的写法,也是官方文档里默认的模块定义写法。AMD 的 API 默认是一个当多个用,CMD 的 API 严格区分,推崇职责单一。比如 AMD 里,require 分全局 require 和局部 require,都叫 require。CMD 里,没有全局 require,而是根据模块系统的完备性,提供 seajs.use 来实现模块系统的加载启动。CMD 里,每个 API 都简单纯粹。还有一些细节差异,具体看这个规范的定义就好,就不多说了。参考:使用 AMD、CommonJS 及 ES Harmony 编写模块化的 JavaScript ...

April 12, 2019 · 5 min · jiezi

手摸手教你搭建 Travis CI 持续集成和自动化部署

很早之前我就在用 Travis CI 做持续集成了,虽然只是停留在 zhuang bi 的阶段,但或多或少也保证了代码的提交质量。最近在写一个 《JavaScript API 全解析》系列的 Book,需要经常把文章部署到服务器上,手动部署实在是烦,索性花了一天时间研究了一下自动化部署。这篇文章是对 Travis CI 持续集成和自动化部署的总结,以飨社区。前戏Travis CI 目前有两个网站,一个是 travis-ci.com,另一个是 travis-ci.org. 前者用于企业级和私有仓库,后者用于开源的公有仓库。实际上 free plan 也可以使用 travis-ci.com,但优先级很低,跑个自动化动辄两个小时,因此我们使用 travis-ci.org.首先打开 Travis CI 官网,并用 GitHub 账号登录,授权后 Travis CI 会同步你的仓库信息。接下来把需要做自动化的工程授权给 Travis CI.最好有一台 Linux 的服务器,我的是 Cent OS 7.6.x 64bit.我们点开一个工程,再切到设置,可以看到在 push 代码和 PR 时都会触发持续集成,当然可以根据需求手动配置。持续集成为了让持续集成像那么回事儿,我们先在 master 上切一个 develop 分支,再在 develop 上切一个 featur/ci 分支。接着我们再用 Jest 写几个测试用例,注意如果项目中没有测试脚本而 .travis.yml 文件里面包含 yarn test,自动化 一定 报错。关于 Jest 这里不详细说,只贴出几个示例代码。import * as utils from ‘../utils/util’;test(‘should get right date’, () => { expect(utils.formatJSONDate(‘2019-03-10T04:15:40.629Z’)).toBe( ‘2019-03-10 12:15:40’, );});test(‘should get right string’, () => { expect(utils.upperFirstLetter(‘AFTERNOON’)).toBe(‘Afternoon’); expect(utils.upperFirstLetter(‘YANCEY_LEO’)).toBe(‘Yancey Leo’);});然后我们在工程的根目录下新建一个文件 .travis.yml,并复制下面的代码。language: node_jsnode_js: - 8branchs: only: - mastercache: directories: - node_modulesinstall: - yarn installscripts: - yarn test - yarn build简单解释一下,工程使用 Node.js 8.x,并且只在 master 分支有变动时触发 自动化部署(正常的提交、PR 都会正常走持续集成),接着将 node_modules 缓存起来(你懂的),最后安装依赖、跑测试脚本、在沙箱部署。因此,理论上只要跑通这套流程,我们就可以放心的部署到真实环境了。提交一下代码,并 pull request 到 develop 分支。在此过程中我们触发了 push 和 PR,所以会跑两个 CI。待到两个都成功跑完后,我们就可以放心的合到 develop 分支了。(这里我还做了代码质量检测,有兴趣可以戳 Codacy)最后我们回到 Travis CI 的官网,可以看到一套完整的构建流程:安装依赖 -> 测试 -> 沙箱部署持续部署创建 rsa 对,并给予权限首先登录你的服务器,一般来讲我们不会直接在 root 上操作,所以这里新增一个 caddy 的用户 。具体怎样在 Linux 新建用户请自行谷歌。接下来 cd 到 ~/.ssh,看看有没有一对 id_rsa 和 id_rsa.pub,如果没有就用 ssh-keygen 生成。给予 .ssh 文件夹 700 权限,给予 .ssh 里的文件 600 权限。(看下面这张图,你的文件夹里可能暂时没有 authorized_keys、 known_host、config 这三个文件,后面会说到。)$ sudo chmod 700 ~/.ssh/$ sudo chmod 600 ~/.ssh/将生成的公钥添加到受信列表进入到 .ssh 文件夹里,执行下面的命令,可以看到公钥被添加到受信列表。$ cat id_rsa.pub >> authorized_keys$ cat authorized_keys测试登录在 .ssh 目录下创建一个文件 config,输入如下代码并保存。Host testHostName 当前服务器的IPUser 当前用户名IdentitiesOnly yesIdentityFile ~/.ssh/id_rsa因为 authorized_keys 和 config 文件都是新增的,它们还没被赋予 600 权限,所以重新执行一遍 sudo chmod 600 ~/.ssh/.然后我们输入 ssh test,不出意外会重新登录 ssh。如果你的公钥从来没有被使用过,会提示 Are you sure you want to continue connecting (yes/no)? ,输入 yes 后也会正常重新登录,并且在.ssh 文件夹下还会生成一个 known_hosts 文件.安装 Ruby因为 Travis 客户端是用 Ruby 写的,所以我们得先安装 Ruby.首先安装需要的依赖包:$ yum install gcc-c++ patch readline readline-devel zlib zlib-devel \ libyaml-devel libffi-devel openssl-devel make \ bzip2 autoconf automake libtool bison iconv-devel sqlite-devel接下来安装 RVM,并载入 RVM 环境。RVM 是 Ruby 的版本管理工具,类似于 Node 的 NVM.$ gpg –keyserver hkp://keys.gnupg.net –recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3$ \curl -sSL https://get.rvm.io | bash -s stable# 载入 rvm 环境$ source ~/.rvm/scripts/rvm安装完之后输入 rvm -v 做下检查,如果有 rvm 1.29.1 (latest) by Michal Papis, Piotr Kuczynski, Wayne E. Seguin [https://rvm.io/] 的字样证明安装成功。最后安装 Ruby,这里选择 v2.4.1 版本,安装需要一段时间,完成后记得将此版本设为默认。$ rvm install 2.4.1$ rvm 2.4.1 –default执行一下 ruby -v 和 gem -v,如果和下图差不多证明安装成功。安装 Travis 客户端执行下面的命令以安装 Travis 客户端。$ gem install travis安装完成后执行 travis,它会让你安装相应的 Shell, 输入 yes 即可。配置免密登录将你的工程克隆下来,并进入到工程目录,然后登录你的 GitHub 账号。$ travis login –auto执行下面这句,它会利用服务器的私钥加密成一个叫做 id_rsa.enc 的文件,这个文件被用于 travis 登录你服务器的凭证,从而达到免密的目的。$ travis encrypt-file ~/.ssh/id_rsa –add我们执行一下 ll,可以看到根目录下多出一个 id_rsa.enc 文件来,并且 cat .travis.yml,发现多出了 before_install.为了更好地组织代码,我们在项目的根目录新建一个文件夹 .travis,然后将 id_rsa.enc 放到里面。配置 after_success 钩子在写这一小节之前,我们先看一看 Travis 的生命周期:before_install 安装依赖前install 安装依赖时before_script 执行脚本前script 执行脚本时after_success 或 after_failure 执行脚本成功(失败)后before_deploy 部署前deploy 部署时after_deploy 部署后after_script 执行脚本后因此 after_success 可用在成功通过测试脚本之后执行部署相关的脚本。当然细一点可以使用 deploy 相关的钩子,这里不做太复杂。打开 .travis.yml文件,直接上全部代码。language: node_jssudo: truenode_js: - 8branchs: only: - master# 这里填写服务器的ip,若端口号不是22,后面要注明端口号addons: ssh_known_hosts: - 你的服务器IPcache: directories: - node_modulesbefore_install: # 因为我们把 id_rsa.enc 移到了.travis 文件夹下,所以 -in 后面要改成 .travis/id_rsa.enc # 其次,-out 后面自动生成的是 ~/.ssh/id_rsa,要把 \ 去掉,否则会编译失败 - openssl aes-256-cbc -K $encrypted_XXXXXXXXXXXX_key -iv $encrypted_XXXXXXXXXXXX_iv -in .travis/id_rsa.enc -out ~/.ssh/id_rsa -d # 开启 ssh-agent,即允许使用 ssh 命令 - eval “$(ssh-agent -s)” # 给予 id_rsa 文件权限,避免警告 - chmod 600 ~/.ssh/id_rsa # 将私钥添加到 ssh - ssh-add ~/.ssh/id_rsainstall: - yarn installscripts: - yarn test - yarn buildafter_success: # 登录服务器,执行部署脚本,其实最好把后面一串写成 shell 文件 - ssh caddy@你的服务器IP -o StrictHostKeyChecking=no ‘cd /var/www/jsapi/JavaScript-APIs-Set && git pull && yarn install && yarn build’走一遍正式的流程至此,搭建 Travis CI 持续集成和自动化部署就算完成了,可能不太严谨,但基本是这么一个思路。下面我们梳理一遍流程。我们先在 feature/ci 分支修改一段代码,提交分支,并 PR 到 develop,此时会运行两个 CI。当两个 CI 都跑通了,我们可以放心的 merge request 到 develop 分支。接下来让 develop PR 到 master,此时会运行两个 CI(一个是 develop 分支,一个是测试合并到 master 的 CI)。当两个 CI 都跑通了,我们可以放心的 merge request 到 master 分支。merge request 之后会跑最后一个流程, 也就是自动部署,部署成功后线上代码就会更新了。加入徽章别忘了把 build passing 徽章添加到你的 README.md 文件中。最后不知道你有没有发现,Travis CI 支持 LGBT…以上、よろしく。参考How to Encrypt/Decrypt SSH Keys for DeploymentTravis-CI 自动化测试并部署至自己的 CentOS 服务器CentOS 7 使用 rvm 安装 ruby 搭建 jekyll 环境 ...

March 28, 2019 · 3 min · jiezi

手把手带你撸一个cli工具

你有没有遇到过在没有vue-cli、create-react-app这样子的脚手架的时候一个文件一个文件的去拷贝老项目的配置文件。最近,笔者就在为组里的框架去做一套基本的cli工具。通过这边文章,笔者希望大家都能简单的去实现一个属于自己的脚手架工具。原文链接: https://juejin.im/user/57ac15…做好准备工作首先,我们需要去新建一个项目并初始化package.jsonmkdir my-cli && cd my-clinpm init然后我们需要在项目中新建bin文件夹,并将package.json中提供一个bin字段并指向我们的bin文件夹下,这样通过npm我们就可以实现指令的软链了。“bin”: { “mycli”: “bin/mycli”},在mycli中,我们要在头部增加这样一句注释,作用是"指定由哪个解释器来执行脚本"。#!/usr/bin/env nodeconsole.log(‘hello world’);接下来,全局安装我们这个包,这样我们就可以直接在本地使用mycli这个指令了。sudo npm install -g提供基本模版既然我们要去做一个初始化项目的cli,那么项目模版就必不可少了,笔者在这里提前准备了一个demo的项目目录模版,这里就不展开赘述了。编写逻辑其实核心逻辑很简单,就是通过控制台获取到用户的一些自定义选项,然后根据选项去从本地或者远程仓库拿到我们提前准备好的模版,将配置写入模版并最后拷贝模版到本地就行了。我们在src下新增creator.js文件,这个文件导出一个Creator的类。在这个类中现在仅需要三个简单的方法:init用于初始化、ask用于和命令行交互获取用户选择输入的数据、write用于调用模版的构建方法去执行拷贝文件写数据的任务。class Creator { constructor() { // 存储命令行获取的数据,作为demo这里只要这两个; this.options = { name: ‘’, description: ‘’, }; } // 初始化; init() {} // 和命令行交互; ask() {} // 拷贝&写数据; write() {}}module.exports = Creator;先去完善init方法,这个方法里我们仅需要调用ask方法和命令行交互并做一些提示即可(可以通过chalk这个库去丰富我们的命令行交互色彩)// …init() { console.log(chalk.green(‘my cli 开始’)); console.log(); this.ask();}// …接下来是ask方法,在这个方法中,我们需要根据提示引导用户输入问题并获取用户的输入,这里用到inquirer这个库来和命令行交互。// …ask() { // 问题 const prompt = []; prompt.push({ type: ‘input’, name: ’name’, message: ‘请输入项目名称’, validate(input) { if (!input) { return ‘请输入项目名称!’; } if (fs.existsSync(input)) { return ‘项目名已重复!’ } return true; } }); prompt.push({ type: ‘input’, name: ‘description’, message: ‘请输入项目描述’, }); // 返回promise return inquirer.prompt(prompt);}// …修改刚才的init方法,将ask方法改为Promise调用。init() { console.log(chalk.green(‘my cli 开始’)); console.log(); this.ask().then((answers) => { this.options = Object.assign({}, this.options, answers); console.log(this.options); });}现在我们去命令行试一下,修改bin/mycli文件,然后去运行mycli命令。#!/usr/bin/env nodeconst Creator = require(’../src/creator.js’);const project = new Creator();project.init();在和用户交互完毕并获取到数据后,我们要做的就是去调用write方法执行拷贝构建了。考虑到日后可能增加很多的模版目录,不妨我们将每一类的模版拷贝构建工作放到模版中的脚本去做,从而增大可扩展性,新增template/index.js文件。接下来首先根据项目目录结构创建文件夹(注意区分项目的执行目录和项目目录的关系)。module.exports = function(creator, options, callback) { const { name, description } = options; // 获取当前命令的执行目录,注意和项目目录区分 const cwd = process.cwd(); // 项目目录 const projectPath = path.join(cwd, name); const buildPath = path.join(projectPath, ‘build’); const pagePath = path.join(projectPath, ‘page’); const srcPath = path.join(projectPath, ‘src’); // 新建项目目录 // 同步创建目录,以免文件目录不对齐 fs.mkdirSync(projectPath); fs.mkdirSync(buildPath); fs.mkdirSync(pagePath); fs.mkdirSync(srcPath); callback();}然后回到creator.js文件,在Creator中的write调用这个方法。// …init() { console.log(chalk.green(‘my cli 开始’)); console.log(); this.ask().then((answers) => { this.options = Object.assign({}, this.options, answers); this.write(); });}// …write() { console.log(chalk.green(‘my cli 构建开始’)); const tplBuilder = require(’../template/index.js’); tplBuilder(this, this.options, () => { console.log(chalk.green(‘my cli 构建完成’)); console.log(); console.log(chalk.grey(开始项目: cd ${this.options.name } &amp;&amp; npm install)); });}// …在开启文件拷贝写数据之前,我们需要用到两个库mem-fs和mem-fs-editor,前者可以帮助我们在内存中创建一个临时的文件store,后者可以以ejs的形式去编辑我们的文件。现在constructor中初始化store。constructor() { // 创建内存store const store = memFs.create(); this.fs = memFsEditor.create(store); this.options = { name: ‘’, description: ‘’, }; this.rootPath = path.resolve(__dirname, ‘../’); this.tplDirPath = path.join(this.rootPath, ’template’);}接下来在Creator中增加两个方法copy和copyTpl分别用于直接拷贝文件和拷贝文件并注入数据。getTplPath(file) { return path.join(this.tplDirPath, file);}copyTpl(file, to, data = {}) { const tplPath = this.getTplPath(file); this.fs.copyTpl(tplPath, to, data);}copy(file, to) { const tplPath = this.getTplPath(file); this.fs.copy(tplPath, to);}然后我们根据ejs的语法修改模版中的package.json文件以实现数据注入的功能{ “name”: “<%= name %>”, “version”: “1.0.0”, “description”: “<%= description %>”, “main”: “index.js”, “scripts”: {}, “author”: “”, “license”: “ISC”}回到template/index.js中,对模版中的文件进行相应的拷贝和数据注入操作,最后打印一些可视化的信息。module.exports = function(creator, options, callback) { const { name, description } = options; // 获取当前命令的执行目录,注意和项目目录区分 const cwd = process.cwd(); // 项目目录 const projectPath = path.join(cwd, name); const buildPath = path.join(projectPath, ‘build’); const pagePath = path.join(projectPath, ‘page’); const srcPath = path.join(projectPath, ‘src’); // 新建项目目录 // 同步创建目录,以免文件目录不对齐 fs.mkdirSync(projectPath); fs.mkdirSync(buildPath); fs.mkdirSync(pagePath); fs.mkdirSync(srcPath); creator.copyTpl(‘packagejson’, path.join(projectPath, ‘package.json’), { name, description, }); creator.copy(‘build/build.js’, path.join(buildPath, ‘build.js’)); creator.copy(‘page/index.html’, path.join(pagePath, ‘index.html’)); creator.copy(‘src/index.js’, path.join(srcPath, ‘index.js’)); creator.fs.commit(() => { console.log(); console.log(${chalk.grey(创建项目: ${name})} ${chalk.green('✔ ')}); console.log(${chalk.grey(创建目录: ${name}/build)} ${chalk.green('✔ ')}); console.log(${chalk.grey(创建目录: ${name}/page)} ${chalk.green('✔ ')}); console.log(${chalk.grey(创建目录: ${name}/src)} ${chalk.green('✔ ')}); console.log(${chalk.grey(创建文件: ${name}/build/build.js)} ${chalk.green('✔ ')}); console.log(${chalk.grey(创建文件: ${name}/page/index.html)} ${chalk.green('✔ ')}); console.log(${chalk.grey(创建文件: ${name}/src/index.js)} ${chalk.green('✔ ')}); callback(); });}执行mycli指令创建项目,一个简单的cli就完成了。结语到此,一个简单的cli就制作完成了,大家可以参考vue-cli、create-react-app等优秀的cli适当的扩展自己的cli工具。 ...

March 26, 2019 · 2 min · jiezi

Webpack5.0 新特性尝鲜实战

作者:志佳老师本文首发微信公众号:jingchengyideng欢迎关注,每天都给你推送新鲜的前端技术文章在老袁写这篇文章的时候,v5版本仍然处于早期阶段,可能仍然有问题。而且作为一个major版本,其中有一些breaking changes,可能会导致一些配置和插件不工作。但这并无妨碍我们去开始对changelog上的新特性进行尝鲜实战。大家如果遇到什么问题可以移步到这进行反馈。另外有关于Webpack4的配置和Compiler->Compilation->Chunk->Module->Template整体运行原理国内外有很多优秀的文章我这里就不一一展开了。接下来天也不早了人也不少了,让我们一起干点正事。(本图截自twitter列出了接下来v5版本的改进,嗯…感觉屏幕还是小了一点)(本图截自github,截图时间为3月12日。我们看到目前开发进度到了57%)一顿操作猛如虎指南升级你的Node到8(V5将Node.js版本从6升级到了8)npm install webpack@next —save-devnpm install webpack-cli —save-devpackage.json添加 “dev”: “webpack –mode development"package.json 添加 “prod”: “webpack –mode production"开始Webpack V5尝鲜之旅新建src文件夹,然后新建index.js。简单的写了一句 console.log(“Hello Webpack5”)1. dist打包文件测评#激动的心 颤抖的手npm run dev我的内心毫无波澜……卒????…好了,到这里结束了。散了吧~3个小时以后…我依旧心不死 发现了这个issues解决。让我们一起看看运行成功之后V5和V4的对比图<u>V5打包到dist的main.js</u><u>V4打包到dist的main.js</u><u>V5打包过程</u><u>V4打包过程</u>没有文化的我只能说一句,哎呀我去!!体积小了一半之多,而且那个startup函数简直骚气的一批????2. 让人揪心的按需加载以前当我们想在index.js内部 import(./async.js”).then(…)的时候,如果我们什么也不加。V4会默认对这些文件生成一堆0.js,1.js,2.js…是多么的整齐.所以我们需要使用import(/* webpackChunkName: “name” */ “module”) 才能化解这份尴尬。今天V5可以在开发模式中启用了一个新命名的块 id 算法,该算法提供块(以及文件名)可读的引用。 模块 ID 由其相对于上下文的路径确定。 块 ID 是由块的内容决定的,所以你不再需要使用Magic Comments。//src文件夹index.jsimport(”./async.js").then(()=>{ console.log(.data);})console.log(“Hello Webpack5”)//src文件夹async.jsconst data = “异步数据????";export default data; 再次编译之后src_async_js.js 就躺在了dist里????。如果这个时候去执行 npm run prod 会在dist里出现一个已数字开头的js文件。比如我的是61.js,你可能非常好奇,这是什么鬼❓3. moduleIds & chunkIds得已确定首先我们改造一下上面的文件。//src文件夹index.jsimport(”./async.js").then(() => { console.log(.data);})import("./async2.js").then(() => { console.log(.data2);})console.log(“Hello Webpack5”)//src文件夹async2.jsimport common from “./common.js"console.log(common)const data2 = “异步数据????";export default data2;在V4的版本中async.js、async2.js会被一次分配给一个chunkId。然后生成的main.js根据chunkId加载对应的文件,但是悲剧的事如果此时我删掉 import(”./async.js”).then(() => {console.log(.data);}) 这一行的话会导致async2进行上位也就是原来的1变成了0。如下图:利用BeyondCompare我们也清晰的看到了main的变化。有同学说这还不好办,我又可以用Magic Comments、也可以用一些插件就可以固定住他的 moduleIds & chunkIds。是的你说的没错,但是V5将不需要引入任何的外力,如上我们遇到prod陌生的带数字的JS,就是为了增强long-term caching,增加了新的算法,并在生产模式下使用以下配置开启。这些算法以确定性的方式为模块和数据块分配非常短(3或4个字符)的数字 id。//Webpack4生产环境的默认配置module.exports = { optimization:{ chunkIds: “deterministic”, moduleIds: “deterministic” }}//Webpack4生产环境的默认配置module.exports = { optimization:{ chunkIds: “natural”, moduleIds: “size” }}如果你觉得这些新特性让你不爽,你依旧可以设置 optimization: { chunkIds: ’named’ } 它是兼容的,这一点还是值得点赞的。4. 饱受诟病的编译速度Webpack的编译速度相信是很多同学比较头痛的问题,当然我们也有很多优化的办法。比如HappyPack、Cache-loader、排除node_modules、多线程压缩甚至可以采用分布式编译等等。其实Webpack编译慢还跟他的laoder机制不无关系,比如string->ast->string这一点跟Parcel确实有些差距 ????。那在V5的版本中都带来些哪些改变呢?其实你只要在配置文件中加上这样一句:module.exports = { cache: { type: “filesystem” }}其实cache在V4版本中就有cache,不过如上这个配置官网上也在说是一个实验性的,也说如果当使用持久缓存时,不再需要cache-loader。 对于 babel cacheDirectory 等也是如此。老袁太忙也没有时间详细的翻所有的pr和源码,不过大致运行了下貌似有的效果????如果哪位大神这里有空翻过了源码也欢迎在评论区讨论????(开启缓存之后的编译速度)5. minSize&maxSize 更好的方式表达在V4版本中默认情况下,仅能处理javascript的大小????module.exports = { optimization: { splitChunks: { cacheGroups: { commons: { chunks: “all”, name: “commons”, minChunks: 1, minSize: “数值”, maxSize: “数值” } } } }}V5版本的变更,这个变更简直是太皮了???? 老袁已经试过了,效果还是蛮不错的。module.exports = { optimization: { splitChunks: { cacheGroups: { commons: { chunks: “all”, name: “commons”, } }, //最小的文件大小 超过之后将不予打包 minSize: { javascript: 0, style: 0, }, //最大的文件 超过之后继续拆分 maxSize: { javascript: 1, //故意写小的效果更明显 style: 3000, } } }}7.编译器的优化如果大家读过Webpack的源码一定知道Compiler的重要性,在Webpack中充斥着大量的钩子和触发事件。在新的版本中,编译器在使用完毕后应该被关闭,因为它们在进入或退出空闲状态时,拥有这些状态的 hook。 插件可以用这些 hook 来执行不太重要的工作(比如:持久性缓存把缓存慢慢地存储到磁盘上)。同时插件的作者应该预见到某些用户可能会忘记关闭编译器,所以 当编译器关闭所有剩下的工作时应尽快完成。 然后回调将会通知已彻底完成。 当你升级到 v5 时,请确保在完成工作后使用 Node.js API 调用 Compiler.close。8. Node.js polyfills 自动被移除过去,Webpack 4版本附带了大多数 Node.js 核心模块的 polyfills,一旦前端使用了任何核心模块,这些模块就会自动应用,但是其实有些是不必要的。 V5中的尝试是自动停止 polyfilling 这些核心模块,并侧重于前端兼容的模块。当迁移到 v5时,最好尽可能使用前端兼容的模块,并尽可能手动添加核心模块的polyfills。 Webpack鼓励大家多提交自己的意见,因为这个更改可能会也可能不会进入最终的 v5版本。现在微前端已经在很多国内的团队大量应用,老袁个人觉得这个改动对于前端更专注开发模块更有益处。在本文开头的时候,我们列出了一张作者演讲的图有关于Webpack的改动。大家可以点击这里看到全部。新的版本变动必将引起很多插件会出问题,但是V5的性能改进是我们更加期待的。最后我想说天下武功出少林,天下技术出基础。大家夯实基础多悟原理才能跟的上变化如此快的前端娱乐圈。作者 志佳老师 2019 年 03月 12日欢迎继续阅读本专栏其它高赞文章:12个令人惊叹的CSS实验项目世界顶级公司的前端面试都问些什么CSS Flexbox 可视化手册过节很无聊?还是用 JavaScript 写一个脑力小游戏吧!从设计者的角度看 ReactCSS粘性定位是怎样工作的一步步教你用HTML5 SVG实现动画效果程序员30岁前月薪达不到30K,该何去何从7个开放式的前端面试题React 教程:快速上手指南本文首发微信公众号:jingchengyideng欢迎扫描二维码关注公众号,每天都给你推送新鲜的前端技术文章 ...

March 13, 2019 · 2 min · jiezi

Webpack系列-第三篇流程杂记

系列文章Webpack系列-第一篇基础杂记 Webpack系列-第二篇插件机制杂记 Webpack系列-第三篇流程杂记前言本文章个人理解, 只是为了理清webpack流程, 没有关注内部过多细节, 如有错误, 请轻喷~调试1.使用以下命令运行项目,./scripts/build.js是你想要开始调试的地方node –inspect-brk ./scripts/build.js –inline –progress2.打开chrome://inspect/#devices即可调试流程图入口入口处在bulid.js,可以看到其中的代码是先实例化webpack,然后调用compiler的run方法。function build(previousFileSizes) { let compiler = webpack(config); return new Promise((resolve, reject) => { compiler.run((err, stats) => { … });}entry-option(compiler)webpack.jswebpack在node_moduls下面的\webpack\lib\webpack.js(在此前面有入口参数合并),找到该文件可以看到相关的代码如下const webpack = (options, callback) => { …… let compiler; // 处理多个入口 if (Array.isArray(options)) { compiler = new MultiCompiler(options.map(options => webpack(options))); } else if (typeof options === “object”) { // webpack的默认参数 options = new WebpackOptionsDefaulter().process(options); console.log(options) // 见下图 // 实例化compiler compiler = new Compiler(options.context); compiler.options = options; // 对webpack的运行环境处理 new NodeEnvironmentPlugin().apply(compiler); // 根据上篇的tabpable可知 这里是为了注册插件 if (options.plugins && Array.isArray(options.plugins)) { for (const plugin of options.plugins) { plugin.apply(compiler); } } // 触发两个事件点 environment/afterEnviroment compiler.hooks.environment.call(); compiler.hooks.afterEnvironment.call(); // 设置compiler的属性并调用默认配置的插件,同时触发事件点entry-option compiler.options = new WebpackOptionsApply().process(options, compiler); } else { throw new Error(“Invalid argument: options”); } if (callback) { …… compiler.run(callback); } return compiler;};可以看出options保存的就是本次webpack的一些配置参数,而其中的plugins属性则是webpack中最重要的插件。new WebpackOptionsApply().processprocess(options, compiler) { let ExternalsPlugin; compiler.outputPath = options.output.path; compiler.recordsInputPath = options.recordsInputPath || options.recordsPath; compiler.recordsOutputPath = options.recordsOutputPath || options.recordsPath; compiler.name = options.name; compiler.dependencies = options.dependencies; if (typeof options.target === “string”) { let JsonpTemplatePlugin; let FetchCompileWasmTemplatePlugin; let ReadFileCompileWasmTemplatePlugin; let NodeSourcePlugin; let NodeTargetPlugin; let NodeTemplatePlugin; switch (options.target) { case “web”: JsonpTemplatePlugin = require("./web/JsonpTemplatePlugin"); FetchCompileWasmTemplatePlugin = require("./web/FetchCompileWasmTemplatePlugin"); NodeSourcePlugin = require("./node/NodeSourcePlugin"); new JsonpTemplatePlugin().apply(compiler); new FetchCompileWasmTemplatePlugin({ mangleImports: options.optimization.mangleWasmImports }).apply(compiler); new FunctionModulePlugin().apply(compiler); new NodeSourcePlugin(options.node).apply(compiler); new LoaderTargetPlugin(options.target).apply(compiler); break; case “webworker”:…… …… } } new JavascriptModulesPlugin().apply(compiler); new JsonModulesPlugin().apply(compiler); new WebAssemblyModulesPlugin({ mangleImports: options.optimization.mangleWasmImports }).apply(compiler); new EntryOptionPlugin().apply(compiler); // 触发事件点entry-options并传入参数 context和entry compiler.hooks.entryOption.call(options.context, options.entry); new CompatibilityPlugin().apply(compiler); …… new ImportPlugin(options.module).apply(compiler); new SystemPlugin(options.module).apply(compiler);}run(compiler)调用run时,会先在内部触发beforeRun事件点,然后再在读取recodes(关于records可以参考该文档)之前触发run事件点,这两个事件都是异步的形式,注意run方法是实际上整个webpack打包流程的入口。可以看到,最后调用的是compile方法,同时传入的是onCompiled函数run(callback) { if (this.running) return callback(new ConcurrentCompilationError()); const finalCallback = (err, stats) => { …… }; this.running = true; const onCompiled = (err, compilation) => { …. }; this.hooks.beforeRun.callAsync(this, err => { if (err) return finalCallback(err); this.hooks.run.callAsync(this, err => { if (err) return finalCallback(err); this.readRecords(err => { if (err) return finalCallback(err); this.compile(onCompiled); }); }); });}compile(compiler)compile方法主要上触发beforeCompile、compile、make等事件点,并实例化compilation,这里我们可以看到传给compile的newCompilationParams参数, 这个参数在后面相对流程中也是比较重要,可以在这里先看一下compile(callback) { const params = this.newCompilationParams(); // 触发事件点beforeCompile,并传入参数CompilationParams this.hooks.beforeCompile.callAsync(params, err => { if (err) return callback(err); // 触发事件点compile,并传入参数CompilationParams this.hooks.compile.call(params); // 实例化compilation const compilation = this.newCompilation(params); // 触发事件点make this.hooks.make.callAsync(compilation, err => { …. }); });}newCompilationParams返回的参数分别是两个工厂函数和一个Set集合newCompilationParams() { const params = { normalModuleFactory: this.createNormalModuleFactory(), contextModuleFactory: this.createContextModuleFactory(), compilationDependencies: new Set() }; return params;}compilation(compiler)从上面的compile方法看, compilation是通过newCompilation方法调用生成的,然后触发事件点thisCompilation和compilation,可以看出compilation在这两个事件点中最早当成参数传入,如果你在编写插件的时候需要尽快使用该对象,则应该在该两个事件中进行。createCompilation() { return new Compilation(this);}newCompilation(params) { const compilation = this.createCompilation(); compilation.fileTimestamps = this.fileTimestamps; compilation.contextTimestamps = this.contextTimestamps; compilation.name = this.name; compilation.records = this.records; compilation.compilationDependencies = params.compilationDependencies; // 触发事件点thisCompilation和compilation, 同时传入参数compilation和params this.hooks.thisCompilation.call(compilation, params); this.hooks.compilation.call(compilation, params); return compilation;}下面是打印出来的compilation属性 关于这里为什么要有thisCompilation这个事件点和子编译器(childCompiler),可以参考该文章 总结起来就是:子编译器拥有完整的模块解析和chunk生成阶段,但是少了某些事件点,如"make", “compile”, “emit”, “after-emit”, “invalid”, “done”, “this-compilation”。 也就是说我们可以利用子编译器来独立(于父编译器)跑完一个核心构建流程,额外生成一些需要的模块或者chunk。make(compiler)从上面的compile方法知道, 实例化Compilation后就会触发make事件点了。 触发了make时, 因为webpack在前面实例化SingleEntryPlugin或者MultleEntryPlugin,SingleEntryPlugin则在其apply方法中注册了一个make事件,apply(compiler) { compiler.hooks.compilation.tap( “SingleEntryPlugin”, (compilation, { normalModuleFactory }) => { compilation.dependencyFactories.set( SingleEntryDependency, normalModuleFactory // 工厂函数,存在compilation的dependencyFactories集合 ); } ); compiler.hooks.make.tapAsync( “SingleEntryPlugin”, (compilation, callback) => { const { entry, name, context } = this; const dep = SingleEntryPlugin.createDependency(entry, name); // 进入到addEntry compilation.addEntry(context, dep, name, callback); } );}事实上addEntry调用的是Comilation._addModuleChain,acquire函数比较简单,主要是处理module时如果任务太多,就将moduleFactory.create存入队列等待_addModuleChain(context, dependency, onModule, callback) { …… // 取出对应的Factory const Dep = /** @type {DepConstructor} */ (dependency.constructor); const moduleFactory = this.dependencyFactories.get(Dep); …… this.semaphore.acquire(() => { moduleFactory.create( { contextInfo: { issuer: “”, compiler: this.compiler.name }, context: context, dependencies: [dependency] }, (err, module) => { …… } ); }); }moduleFactory.create则是收集一系列信息然后创建一个module传入回调buildModule(compilation)回调函数主要上执行buildModule方法this.buildModule(module, false, null, null, err => { …… afterBuild();});buildModule(module, optional, origin, dependencies, thisCallback) { // 处理回调函数 let callbackList = this._buildingModules.get(module); if (callbackList) { callbackList.push(thisCallback); return; } this._buildingModules.set(module, (callbackList = [thisCallback])); const callback = err => { this._buildingModules.delete(module); for (const cb of callbackList) { cb(err); } }; // 触发buildModule事件点 this.hooks.buildModule.call(module); module.build( this.options, this, this.resolverFactory.get(“normal”, module.resolveOptions), this.inputFileSystem, error => { …… } ); }build方法中调用的是doBuild,doBuild又通过runLoaders获取loader相关的信息并转换成webpack需要的js文件,最后通过doBuild的回调函数调用parse方法,创建依赖Dependency并放入依赖数组return this.doBuild(options, compilation, resolver, fs, err => { // 在createLoaderContext函数中触发事件normal-module-loader const loaderContext = this.createLoaderContext( resolver, options, compilation, fs ); ….. const handleParseResult = result => { this._lastSuccessfulBuildMeta = this.buildMeta; this._initBuildHash(compilation); return callback(); }; try { // 调用parser.parse const result = this.parser.parse( this._ast || this._source.source(), { current: this, module: this, compilation: compilation, options: options }, (err, result) => { if (err) { handleParseError(err); } else { handleParseResult(result); } } ); if (result !== undefined) { // parse is sync handleParseResult(result); } } catch (e) { handleParseError(e); } });在ast转换过程中也很容易得到了需要依赖的哪些其他模块。succeedModule(compilation)最后执行了module.build的回调函数,触发了事件点succeedModule,并回到Compilation.buildModule函数的回调函数module.build( this.options, this, this.resolverFactory.get(“normal”, module.resolveOptions), this.inputFileSystem, error => { …… 触发了事件点succeedModule this.hooks.succeedModule.call(module); return callback(); });this.buildModule(module, false, null, null, err => { …… // 执行afterBuild afterBuild();});对于当前模块,或许存在着多个依赖模块。当前模块会开辟一个依赖模块的数组,在遍历 AST 时,将 require() 中的模块通过 addDependency() 添加到数组中。当前模块构建完成后,webpack 调用 processModuleDependencies 开始递归处理依赖的 module,接着就会重复之前的构建步骤。 Compilation.prototype.addModuleDependencies = function(module, dependencies, bail, cacheGroup, recursive, callback) { // 根据依赖数组(dependencies)创建依赖模块对象 var factories = []; for (var i = 0; i < dependencies.length; i++) { var factory = _this.dependencyFactories.get(dependencies[i][0].constructor); factories[i] = [factory, dependencies[i]]; } … // 与当前模块构建步骤相同}最后, 所有的模块都会被放入到Compilation的modules里面, 如下: 总结一下:module 是 webpack 构建的核心实体,也是所有 module 的 父类,它有几种不同子类:NormalModule , MultiModule , ContextModule , DelegatedModule 等,一个依赖对象(Dependency,还未被解析成模块实例的依赖对象。比如我们运行 webpack 时传入的入口模块,或者一个模块依赖的其他模块,都会先生成一个 Dependency 对象。)经过对应的工厂对象(Factory)创建之后,就能够生成对应的模块实例(Module)。seal(compilation)构建module后, 就会调用Compilation.seal, 该函数主要是触发了事件点seal, 构建chunk, 在所有 chunks 生成之后,webpack 会对 chunks 和 modules 进行一些优化相关的操作,比如分配id、排序等,并且触发一系列相关的事件点seal(callback) { // 触发事件点seal this.hooks.seal.call(); // 优化 …… this.hooks.afterOptimizeDependencies.call(this.modules); this.hooks.beforeChunks.call(); // 生成chunk for (const preparedEntrypoint of this._preparedEntrypoints) { const module = preparedEntrypoint.module; const name = preparedEntrypoint.name; // 整理每个Module和chunk,每个chunk对应一个输出文件。 const chunk = this.addChunk(name); const entrypoint = new Entrypoint(name); entrypoint.setRuntimeChunk(chunk); entrypoint.addOrigin(null, name, preparedEntrypoint.request); this.namedChunkGroups.set(name, entrypoint); this.entrypoints.set(name, entrypoint); this.chunkGroups.push(entrypoint); GraphHelpers.connectChunkGroupAndChunk(entrypoint, chunk); GraphHelpers.connectChunkAndModule(chunk, module); chunk.entryModule = module; chunk.name = name; this.assignDepth(module); } this.processDependenciesBlocksForChunkGroups(this.chunkGroups.slice()); this.sortModules(this.modules); this.hooks.afterChunks.call(this.chunks); this.hooks.optimize.call(); …… this.hooks.afterOptimizeModules.call(this.modules); …… this.hooks.afterOptimizeChunks.call(this.chunks, this.chunkGroups); this.hooks.optimizeTree.callAsync(this.chunks, this.modules, err => { …… this.hooks.beforeChunkAssets.call(); this.createChunkAssets(); // 生成对应的Assets this.hooks.additionalAssets.callAsync(…) }); }每个 chunk 的生成就是找到需要包含的 modules。这里大致描述一下 chunk 的生成算法:1.webpack 先将 entry 中对应的 module 都生成一个新的 chunk 2.遍历 module 的依赖列表,将依赖的 module 也加入到 chunk 中 3.如果一个依赖 module 是动态引入的模块,那么就会根据这个 module 创建一个新的 chunk,继续遍历依赖 4.重复上面的过程,直至得到所有的 chunkschunk属性图 beforeChunkAssets && additionalChunkAssets(Compilation)在触发这两个事件点的中间时, 会调用Compilation.createCHunkAssets来创建assets,createChunkAssets() { …… // 遍历chunk for (let i = 0; i < this.chunks.length; i++) { const chunk = this.chunks[i]; chunk.files = []; let source; let file; let filenameTemplate; try { // 调用何种Template const template = chunk.hasRuntime() ? this.mainTemplate : this.chunkTemplate; const manifest = template.getRenderManifest({ chunk, hash: this.hash, fullHash: this.fullHash, outputOptions, moduleTemplates: this.moduleTemplates, dependencyTemplates: this.dependencyTemplates }); // [{ render(), filenameTemplate, pathOptions, identifier, hash }] for (const fileManifest of manifest) { ….. } ….. // 写入assets对象 this.assets[file] = source; chunk.files.push(file); this.hooks.chunkAsset.call(chunk, file); alreadyWrittenFiles.set(file, { hash: usedHash, source, chunk }); } } catch (err) { …… } } }createChunkAssets会生成文件名和对应的文件内容,并放入Compilation.assets对象, 这里有四个Template 的子类,分别是 MainTemplate.js , ChunkTemplate.js ,ModuleTemplate.js , HotUpdateChunkTemplate.jsMainTemplate.js: 对应了在 entry 配置的入口 chunk 的渲染模板ChunkTemplate: 动态引入的非入口 chunk 的渲染模板ModuleTemplate.js: chunk 中的 module 的渲染模板HotUpdateChunkTemplate.js: 对热替换模块的一个处理。模块封装(引用自http://taobaofed.org/blog/201…) 模块在封装的时候和它在构建时一样,都是调用各模块类中的方法。封装通过调用 module.source() 来进行各操作,比如说 require() 的替换。MainTemplate.prototype.requireFn = “webpack_require";MainTemplate.prototype.render = function(hash, chunk, moduleTemplate, dependencyTemplates) { var buf = []; // 每一个module都有一个moduleId,在最后会替换。 buf.push(“function " + this.requireFn + “(moduleId) {”); buf.push(this.indent(this.applyPluginsWaterfall(“require”, “”, chunk, hash))); buf.push(”}”); buf.push(""); … // 其余封装操作};最后看看Compilation.assets对象 done(Compiler)最后一步,webpack 调用 Compiler 中的 emitAssets() ,按照 output 中的配置项将文件输出到了对应的 path 中,从而 webpack 整个打包过程结束。要注意的是,若想对结果进行处理,则需要在 emit 触发后对自定义插件进行扩展。总结webpack的内部核心还是在于compilationcompilermodulechunk等对象或者实例。写下这篇文章也有助于自己理清思路,学海无涯~~~引用玩转webpack(一):webpack的基本架构和构建流程 玩转webpack(二):webpack的核心对象 细说 webpack 之流程篇 ...

March 11, 2019 · 5 min · jiezi

Webpack Loader 高手进阶(二)

文章首发于个人github blog: Biu-blog,欢迎大家关注Webpack Loader 详解上篇文章主要讲了 loader 的配置,匹配相关的机制。这篇主要会讲当一个 module 被创建之后,使用 loader 去处理这个 module 内容的流程机制。首先我们来总体的看下整个的流程:在 module 一开始构建的过程中,首先会创建一个 loaderContext 对象,它和这个 module 是一一对应的关系,而这个 module 所使用的所有 loaders 都会共享这个 loaderContext 对象,每个 loader 执行的时候上下文就是这个 loaderContext 对象,所以可以在我们写的 loader 里面通过 this 来访问。// NormalModule.jsconst { runLoaders } = require(’loader-runner’)class NormalModule extends Module { … createLoaderContext(resolver, options, compilation, fs) { const requestShortener = compilation.runtimeTemplate.requestShortener; // 初始化 loaderContext 对象,这些初始字段的具体内容解释在文档上有具体的解释(https://webpack.docschina.org/api/loaders/#this-data) const loaderContext = { version: 2, emitWarning: warning => {…}, emitError: error => {…}, exec: (code, filename) => {…}, resolve(context, request, callback) {…}, getResolve(options) {…}, emitFile: (name, content, sourceMap) => {…}, rootContext: options.context, // 项目的根路径 webpack: true, sourceMap: !!this.useSourceMap, _module: this, _compilation: compilation, _compiler: compilation.compiler, fs: fs }; // 触发 normalModuleLoader 的钩子函数,开发者可以利用这个钩子来对 loaderContext 进行拓展 compilation.hooks.normalModuleLoader.call(loaderContext, this); if (options.loader) { Object.assign(loaderContext, options.loader); } return loaderContext; } doBuild(options, compilation, resolver, fs, callback) { // 创建 loaderContext 上下文 const loaderContext = this.createLoaderContext( resolver, options, compilation, fs ) runLoaders( { resource: this.resource, // 这个模块的路径 loaders: this.loaders, // 模块所使用的 loaders context: loaderContext, // loaderContext 上下文 readResource: fs.readFile.bind(fs) // 读取文件的 node api }, (err, result) => { // do something } ) } …}当 loaderContext 初始化完成后,开始调用 runLoaders 方法,这个时候进入到了 loaders 的执行阶段。runLoaders 方法是由loader-runner这个独立的 npm 包提供的方法,那我们就一起来看下 runLoaders 方法内部是如何运行的。首先根据传入的参数完成进一步的处理,同时对于 loaderContext 对象上的属性做进一步的拓展:exports.runLoaders = function runLoaders(options, callback) { // read options var resource = options.resource || “”; // 模块的路径 var loaders = options.loaders || []; // 模块所需要使用的 loaders var loaderContext = options.context || {}; // 在 normalModule 里面创建的 loaderContext var readResource = options.readResource || readFile; var splittedResource = resource && splitQuery(resource); var resourcePath = splittedResource ? splittedResource[0] : undefined; // 模块实际路径 var resourceQuery = splittedResource ? splittedResource[1] : undefined; // 模块路径 query 参数 var contextDirectory = resourcePath ? dirname(resourcePath) : null; // 模块的父路径 // execution state var requestCacheable = true; var fileDependencies = []; var contextDependencies = []; // prepare loader objects loaders = loaders.map(createLoaderObject); // 处理 loaders // 拓展 loaderContext 的属性 loaderContext.context = contextDirectory; loaderContext.loaderIndex = 0; // 当前正在执行的 loader 索引 loaderContext.loaders = loaders; loaderContext.resourcePath = resourcePath; loaderContext.resourceQuery = resourceQuery; loaderContext.async = null; // 异步 loader loaderContext.callback = null; … // 需要被构建的模块路径,将 loaderContext.resource -> getter/setter // 例如 /abc/resource.js?rrr Object.defineProperty(loaderContext, “resource”, { enumerable: true, get: function() { if(loaderContext.resourcePath === undefined) return undefined; return loaderContext.resourcePath + loaderContext.resourceQuery; }, set: function(value) { var splittedResource = value && splitQuery(value); loaderContext.resourcePath = splittedResource ? splittedResource[0] : undefined; loaderContext.resourceQuery = splittedResource ? splittedResource[1] : undefined; } }); // 构建这个 module 所有的 loader 及这个模块的 resouce 所组成的 request 字符串 // 例如:/abc/loader1.js?xyz!/abc/node_modules/loader2/index.js!/abc/resource.js?rrr Object.defineProperty(loaderContext, “request”, { enumerable: true, get: function() { return loaderContext.loaders.map(function(o) { return o.request; }).concat(loaderContext.resource || “”).join("!"); } }); // 在执行 loader 提供的 pitch 函数阶段传入的参数之一,剩下还未被调用的 loader.pitch 所组成的 request 字符串 Object.defineProperty(loaderContext, “remainingRequest”, { enumerable: true, get: function() { if(loaderContext.loaderIndex >= loaderContext.loaders.length - 1 && !loaderContext.resource) return “”; return loaderContext.loaders.slice(loaderContext.loaderIndex + 1).map(function(o) { return o.request; }).concat(loaderContext.resource || “”).join("!"); } }); // 在执行 loader 提供的 pitch 函数阶段传入的参数之一,包含当前 loader.pitch 所组成的 request 字符串 Object.defineProperty(loaderContext, “currentRequest”, { enumerable: true, get: function() { return loaderContext.loaders.slice(loaderContext.loaderIndex).map(function(o) { return o.request; }).concat(loaderContext.resource || “”).join("!"); } }); // 在执行 loader 提供的 pitch 函数阶段传入的参数之一,包含已经被执行的 loader.pitch 所组成的 request 字符串 Object.defineProperty(loaderContext, “previousRequest”, { enumerable: true, get: function() { return loaderContext.loaders.slice(0, loaderContext.loaderIndex).map(function(o) { return o.request; }).join("!"); } }); // 获取当前正在执行的 loader 的query参数 // 如果这个 loader 配置了 options 对象的话,this.query 就指向这个 option 对象 // 如果 loader 中没有 options,而是以 query 字符串作为参数调用时,this.query 就是一个以 ? 开头的字符串 Object.defineProperty(loaderContext, “query”, { enumerable: true, get: function() { var entry = loaderContext.loaders[loaderContext.loaderIndex]; return entry.options && typeof entry.options === “object” ? entry.options : entry.query; } }); // 每个 loader 在 pitch 阶段和正常执行阶段都可以共享的 data 数据 Object.defineProperty(loaderContext, “data”, { enumerable: true, get: function() { return loaderContext.loaders[loaderContext.loaderIndex].data; } }); var processOptions = { resourceBuffer: null, // module 的内容 buffer readResource: readResource }; // 开始执行每个 loader 上的 pitch 函数 iteratePitchingLoaders(processOptions, loaderContext, function(err, result) { // do something… });}这里稍微总结下就是在 runLoaders 方法的初期会对相关参数进行初始化的操作,特别是将 loaderContext 上的部分属性改写为 getter/setter 函数,这样在不同的 loader 执行的阶段可以动态的获取一些参数。接下来开始调用 iteratePitchingLoaders 方法执行每个 loader 上提供的 pitch 函数。大家写过 loader 的话应该都清楚,每个 loader 可以挂载一个 pitch 函数,每个 loader 提供的 pitch 方法和 loader 实际的执行顺序正好相反。这块的内容在 webpack 文档上也有详细的说明(请戳我)。这些 pitch 函数并不是用来实际处理 module 的内容的,主要是可以利用 module 的 request,来做一些拦截处理的工作,从而达到在 loader 处理流程当中的一些定制化的处理需要,有关 pitch 函数具体的实战可以参见下一篇文档[Webpack 高手进阶-loader 实战] TODO: 链接function iteratePitchingLoaders() { // abort after last loader if(loaderContext.loaderIndex >= loaderContext.loaders.length) return processResource(options, loaderContext, callback); // 根据 loaderIndex 来获取当前需要执行的 loader var currentLoaderObject = loaderContext.loaders[loaderContext.loaderIndex]; // iterate // 如果被执行过,那么直接跳过这个 loader 的 pitch 函数 if(currentLoaderObject.pitchExecuted) { loaderContext.loaderIndex++; return iteratePitchingLoaders(options, loaderContext, callback); } // 加载 loader 模块 // load loader module loadLoader(currentLoaderObject, function(err) { // do something … });}每次执行 pitch 函数前,首先根据 loaderIndex 来获取当前需要执行的 loader (currentLoaderObject),调用 loadLoader 函数来加载这个 loader,loadLoader 内部兼容了 SystemJS,ES Module,CommonJs 这些模块定义,最终会将 loader 提供的 pitch 方法和普通方法赋值到 currentLoaderObject 上:// loadLoader.jsmodule.exports = function (loader, callback) { … var module = require(loader.path) … loader.normal = module loader.pitch = module.pitch loader.raw = module.raw callback() …}当 loader 加载完后,开始执行 loadLoader 的回调:loadLoader(currentLoaderObject, function(err) { var fn = currentLoaderObject.pitch; // 获取 pitch 函数 currentLoaderObject.pitchExecuted = true; if(!fn) return iteratePitchingLoaders(options, loaderContext, callback); // 如果这个 loader 没有提供 pitch 函数,那么直接跳过 // 开始执行 pitch 函数 runSyncOrAsync( fn, loaderContext, [loaderContext.remainingRequest, loaderContext.previousRequest, currentLoaderObject.data = {}], function(err) { if(err) return callback(err); var args = Array.prototype.slice.call(arguments, 1); // Determine whether to continue the pitching process based on // argument values (as opposed to argument presence) in order // to support synchronous and asynchronous usages. // 根据是否有参数返回来判断是否向下继续进行 pitch 函数的执行 var hasArg = args.some(function(value) { return value !== undefined; }); if(hasArg) { loaderContext.loaderIndex–; iterateNormalLoaders(options, loaderContext, args, callback); } else { iteratePitchingLoaders(options, loaderContext, callback); } } );})这里出现了一个 runSyncOrAsync 方法,放到后文去讲,开始执行 pitch 函数,当 pitch 函数执行完后,执行传入的回调函数。我们看到回调函数里面会判断接收到的参数的个数,除了第一个 err 参数外,如果还有其他的参数(这些参数是 pitch 函数执行完后传入回调函数的),那么会直接进入 loader 的 normal 方法执行阶段,并且会直接跳过后面的 loader 执行阶段。如果 pitch 函数没有返回值的话,那么进入到下一个 loader 的 pitch 函数的执行阶段。让我们再回到 iteratePitchingLoaders 方法内部,当所有 loader 上面的 pitch 函数都执行完后,即 loaderIndex 索引值 >= loader 数组长度的时候:function iteratePitchingLoaders () { … if(loaderContext.loaderIndex >= loaderContext.loaders.length) return processResource(options, loaderContext, callback); …}function processResource(options, loaderContext, callback) { // set loader index to last loader loaderContext.loaderIndex = loaderContext.loaders.length - 1; var resourcePath = loaderContext.resourcePath; if(resourcePath) { loaderContext.addDependency(resourcePath); // 添加依赖 options.readResource(resourcePath, function(err, buffer) { if(err) return callback(err); options.resourceBuffer = buffer; iterateNormalLoaders(options, loaderContext, [buffer], callback); }); } else { iterateNormalLoaders(options, loaderContext, [null], callback); }}在 processResouce 方法内部调用 node API readResouce 读取 module 对应路径的文本内容,调用 iterateNormalLoaders 方法,开始进入 loader normal 方法的执行阶段。function iterateNormalLoaders () { if(loaderContext.loaderIndex < 0) return callback(null, args); var currentLoaderObject = loaderContext.loaders[loaderContext.loaderIndex]; // iterate if(currentLoaderObject.normalExecuted) { loaderContext.loaderIndex–; return iterateNormalLoaders(options, loaderContext, args, callback); } var fn = currentLoaderObject.normal; currentLoaderObject.normalExecuted = true; if(!fn) { return iterateNormalLoaders(options, loaderContext, args, callback); } // buffer 和 utf8 string 之间的转化 convertArgs(args, currentLoaderObject.raw); runSyncOrAsync(fn, loaderContext, args, function(err) { if(err) return callback(err); var args = Array.prototype.slice.call(arguments, 1); iterateNormalLoaders(options, loaderContext, args, callback); });}在 iterateNormalLoaders 方法内部就是依照从右到左的顺序(正好与 pitch 方法执行顺序相反)依次执行每个 loader 上的 normal 方法。loader 不管是 pitch 方法还是 normal 方法的执行可为同步的,也可设为异步的(这里说下 normal 方法的)。一般如果你写的 loader 里面可能涉及到计算量较大的情况时,可将你的 loader 异步化,在你 loader 方法里面调用this.async方法,返回异步的回调函数,当你 loader 内部实际的内容执行完后,可调用这个异步的回调来进入下一个 loader 的执行。module.exports = function (content) { const callback = this.async() someAsyncOperation(content, function(err, result) { if (err) return callback(err); callback(null, result); });}除了调用 this.async 来异步化 loader 之外,还有一种方式就是在你的 loader 里面去返回一个 promise,只有当这个 promise 被 resolve 之后,才会调用下一个 loader(具体实现机制见下文):module.exports = function (content) { return new Promise(resolve => { someAsyncOpertion(content, function(err, result) { if (err) resolve(err) resolve(null, result) }) })}这里还有一个地方需要注意的就是,上下游 loader 之间的数据传递过程中,如果下游的 loader 接收到的参数为一个,那么可以在上一个 loader 执行结束后,如果是同步就直接 return 出去:module.exports = function (content) { // do something return content}如果是异步就直接调用异步回调传递下去(参见上面 loader 异步化)。如果下游 loader 接收的参数多于一个,那么上一个 loader 执行结束后,如果是同步那么就需要调用 loaderContext 提供的 callback 函数:module.exports = function (content) { // do something this.callback(null, content, argA, argB)}如果是异步的还是继续调用异步回调函数传递下去(参见上面 loader 异步化)。具体的执行机制涉及到上文还没讲到的 runSyncOrAsync 方法,它提供了上下游 loader 调用的接口:function runSyncOrAsync(fn, context, args, callback) { var isSync = true; // 是否为同步 var isDone = false; var isError = false; // internal error var reportedError = false; // 给 loaderContext 上下文赋值 async 函数,用以将 loader 异步化,并返回异步回调 context.async = function async() { if(isDone) { if(reportedError) return; // ignore throw new Error(“async(): The callback was already called.”); } isSync = false; // 同步标志位置为 false return innerCallback; }; // callback 的形式可以向下一个 loader 多个参数 var innerCallback = context.callback = function() { if(isDone) { if(reportedError) return; // ignore throw new Error(“callback(): The callback was already called.”); } isDone = true; isSync = false; try { callback.apply(null, arguments); } catch(e) { isError = true; throw e; } }; try { // 开始执行 loader var result = (function LOADER_EXECUTION() { return fn.apply(context, args); }()); // 如果为同步的执行 if(isSync) { isDone = true; // 如果 loader 执行后没有返回值,执行 callback 开始下一个 loader 执行 if(result === undefined) return callback(); // loader 返回值为一个 promise 实例,待这个实例被resolve或者reject后执行下一个 loader。这也是 loader 异步化的一种方式 if(result && typeof result === “object” && typeof result.then === “function”) { return result.catch(callback).then(function(r) { callback(null, r); }); } // 如果 loader 执行后有返回值,执行 callback 开始下一个 loader 执行 return callback(null, result); } } catch(e) { // do something }}以上就是对于 module 在构建过程中 loader 执行流程的源码分析。可能平时在使用 webpack 过程了解相关的 loader 执行规则和策略,再配合这篇对于内部机制的分析,应该会对 webpack loader 的使用有更加深刻的印象。文章首发于个人github blog: Biu-blog,欢迎大家关注 ...

March 11, 2019 · 7 min · jiezi

前端开发者必备的Nginx知识

nginx在应用程序中的作用解决跨域请求过滤配置gzip负载均衡静态资源服务器nginx是一个高性能的HTTP和反向代理服务器,也是一个通用的TCP/UDP代理服务器,最初由俄罗斯人Igor Sysoev编写。nginx现在几乎是众多大型网站的必用技术,大多数情况下,我们不需要亲自去配置它,但是了解它在应用程序中所担任的角色,以及如何解决这些问题是非常必要的。下面我将从nginx在企业中的真实应用来解释nginx在应用程序中起到的作用。为了便于理解,首先先来了解一下一些基础知识,nginx是一个高性能的反向代理服务器那么什么是反向代理呢?正向代理与反向代理代理是在服务器和客户端之间假设的一层服务器,代理将接收客户端的请求并将它转发给服务器,然后将服务端的响应转发给客户端。不管是正向代理还是反向代理,实现的都是上面的功能。正向代理正向代理,意思是一个位于客户端和原始服务器(origin server)之间的服务器,为了从原始服务器取得内容,客户端向代理发送一个请求并指定目标(原始服务器),然后代理向原始服务器转交请求并将获得的内容返回给客户端。正向代理是为我们服务的,即为客户端服务的,客户端可以根据正向代理访问到它本身无法访问到的服务器资源。正向代理对我们是透明的,对服务端是非透明的,即服务端并不知道自己收到的是来自代理的访问还是来自真实客户端的访问。反向代理 反向代理(Reverse Proxy)方式是指以代理服务器来接受internet上的连接请求,然后将请求转发给内部网络上的服务器,并将从服务器上得到的结果返回给internet上请求连接的客户端,此时代理服务器对外就表现为一个反向代理服务器。反向代理是为服务端服务的,反向代理可以帮助服务器接收来自客户端的请求,帮助服务器做请求转发,负载均衡等。反向代理对服务端是透明的,对我们是非透明的,即我们并不知道自己访问的是代理服务器,而服务器知道反向代理在为他服务。基本配置配置结构下面是一个nginx配置文件的基本结构:events { }http { server { location path { … } location path { … } } server { … }}main:nginx的全局配置,对全局生效。events:配置影响nginx服务器或与用户的网络连接。http:可以嵌套多个server,配置代理,缓存,日志定义等绝大多数功能和第三方模块的配置。server:配置虚拟主机的相关参数,一个http中可以有多个server。location:配置请求的路由,以及各种页面的处理情况。upstream:配置后端服务器具体地址,负载均衡配置不可或缺的部分。内置变量下面是nginx一些配置中常用的内置全局变量,你可以在配置的任何位置使用它们。| 变量名 | 功能 | | —— | —— | | $host| 请求信息中的Host,如果请求中没有Host行,则等于设置的服务器名 || $request_method | 客户端请求类型,如GET、POST| $remote_addr | 客户端的IP地址 ||$args | 请求中的参数 ||$content_length| 请求头中的Content-length字段 ||$http_user_agent | 客户端agent信息 ||$http_cookie | 客户端cookie信息 ||$remote_addr | 客户端的IP地址 ||$remote_port | 客户端的端口 ||$server_protocol | 请求使用的协议,如HTTP/1.0、·HTTP/1.1` ||$server_addr | 服务器地址 ||$server_name| 服务器名称||$server_port|服务器的端口号|解决跨域先追本溯源以下,跨域究竟是怎么回事。跨域的定义同源策略限制了从同一个源加载的文档或脚本如何与来自另一个源的资源进行交互。这是一个用于隔离潜在恶意文件的重要安全机制。通常不允许不同源间的读操作。同源的定义如果两个页面的协议,端口(如果有指定)和域名都相同,则两个页面具有相同的源。nginx解决跨域的原理例如:前端server的域名为:fe.server.com后端服务的域名为:dev.server.com现在我在fe.server.com对dev.server.com发起请求一定会出现跨域。现在我们只需要启动一个nginx服务器,将server_name设置为fe.server.com,然后设置相应的location以拦截前端需要跨域的请求,最后将请求代理回dev.server.com。如下面的配置:server { listen 80; server_name fe.server.com; location / { proxy_pass dev.server.com; }}这样可以完美绕过浏览器的同源策略:fe.server.com访问nginx的fe.server.com属于同源访问,而nginx对服务端转发的请求不会触发浏览器的同源策略。请求过滤根据状态码过滤error_page 500 501 502 503 504 506 /50x.html; location = /50x.html { #将跟路径改编为存放html的路径。 root /root/static/html; }根据URL名称过滤,精准匹配URL,不匹配的URL全部重定向到主页。location / { rewrite ^.$ /index.html redirect;}根据请求类型过滤。if ( $request_method !~ ^(GET|POST|HEAD)$ ) { return 403; }配置gzipGZIP是规定的三种标准HTTP压缩格式之一。目前绝大多数的网站都在使用 GZIP 传输 HTML、CSS、JavaScript 等资源文件。对于文本文件,GZip 的效果非常明显,开启后传输所需流量大约会降至 1/4 ~ 1/3。并不是每个浏览器都支持gzip的,如何知道客户端是否支持gzip呢,请求头中的Accept-Encoding来标识对压缩的支持。启用gzip同时需要客户端和服务端的支持,如果客户端支持gzip的解析,那么只要服务端能够返回gzip的文件就可以启用gzip了,我们可以通过nginx的配置来让服务端支持gzip。下面的respone中content-encoding:gzip,指服务端开启了gzip的压缩方式。 gzip on; gzip_http_version 1.1; gzip_comp_level 5; gzip_min_length 1000; gzip_types text/csv text/xml text/css text/plain text/javascript application/javascript application/x-javascript application/json application/xml;gzip开启或者关闭gzip模块默认值为 off可配置为 on / offgzip_http_version启用 GZip 所需的 HTTP 最低版本默认值为 HTTP/1.1这里为什么默认版本不是1.0呢?HTTP 运行在 TCP 连接之上,自然也有着跟 TCP 一样的三次握手、慢启动等特性。启用持久连接情况下,服务器发出响应后让TCP连接继续打开着。同一对客户/服务器之间的后续请求和响应可以通过这个连接发送。为了尽可能的提高 HTTP 性能,使用持久连接就显得尤为重要了。HTTP/1.1 默认支持 TCP 持久连接,HTTP/1.0 也可以通过显式指定 Connection: keep-alive 来启用持久连接。对于 TCP 持久连接上的 HTTP 报文,客户端需要一种机制来准确判断结束位置,而在 HTTP/1.0 中,这种机制只有 Content-Length。而在HTTP/1.1 中新增的 Transfer-Encoding: chunked 所对应的分块传输机制可以完美解决这类问题。nginx同样有着配置chunked的属性chunked_transfer_encoding,这个属性是默认开启的。Nginx 在启用了GZip的情况下,不会等文件 GZip 完成再返回响应,而是边压缩边响应,这样可以显著提高 TTFB(Time To First Byte,首字节时间,WEB 性能优化重要指标)。这样唯一的问题是,Nginx 开始返回响应时,它无法知道将要传输的文件最终有多大,也就是无法给出 Content-Length 这个响应头部。所以,在HTTP1.0中如果利用Nginx 启用了GZip,是无法获得 Content-Length 的,这导致HTTP1.0中开启持久链接和使用GZip只能二选一,所以在这里gzip_http_version默认设置为1.1。gzip_comp_level压缩级别,级别越高压缩率越大,当然压缩时间也就越长(传输快但比较消耗cpu)。默认值为 1压缩级别取值为1-9gzip_min_length设置允许压缩的页面最小字节数,Content-Length小于该值的请求将不会被压缩默认值:0当设置的值较小时,压缩后的长度可能比原文件大,建议设置1000以上gzip_types要采用gzip压缩的文件类型(MIME类型)默认值:text/html(默认不压缩js/css)负载均衡什么是负载均衡如上面的图,前面是众多的服务窗口,下面有很多用户需要服务,我们需要一个工具或策略来帮助我们将如此多的用户分配到每个窗口,来达到资源的充分利用以及更少的排队时间。把前面的服务窗口想像成我们的后端服务器,而后面终端的人则是无数个客户端正在发起请求。负载均衡就是用来帮助我们将众多的客户端请求合理的分配到各个服务器,以达到服务端资源的充分利用和更少的请求时间。nginx如何实现负载均衡Upstream指定后端服务器地址列表upstream balanceServer { server 10.1.22.33:12345; server 10.1.22.34:12345; server 10.1.22.35:12345;}在server中拦截响应请求,并将请求转发到Upstream中配置的服务器列表。 server { server_name fe.server.com; listen 80; location /api { proxy_pass http://balanceServer; } }上面的配置只是指定了nginx需要转发的服务端列表,并没有指定分配策略。nginx实现负载均衡的策略轮询策略默认情况下采用的策略,将所有客户端请求轮询分配给服务端。这种策略是可以正常工作的,但是如果其中某一台服务器压力太大,出现延迟,会影响所有分配在这台服务器下的用户。upstream balanceServer { server 10.1.22.33:12345; server 10.1.22.34:12345; server 10.1.22.35:12345;}最小连接数策略将请求优先分配给压力较小的服务器,它可以平衡每个队列的长度,并避免向压力大的服务器添加更多的请求。upstream balanceServer { least_conn; server 10.1.22.33:12345; server 10.1.22.34:12345; server 10.1.22.35:12345;}最快响应时间策略依赖于NGINX Plus,优先分配给响应时间最短的服务器。upstream balanceServer { fair; server 10.1.22.33:12345; server 10.1.22.34:12345; server 10.1.22.35:12345;}客户端ip绑定来自同一个ip的请求永远只分配一台服务器,有效解决了动态网页存在的session共享问题。upstream balanceServer { ip_hash; server 10.1.22.33:12345; server 10.1.22.34:12345; server 10.1.22.35:12345;}静态资源服务器location ~ .(png|gif|jpg|jpeg)$ { root /root/static/; autoindex on; access_log off; expires 10h;# 设置过期时间为10小时 }匹配以png|gif|jpg|jpeg为结尾的请求,并将请求转发到本地路径,root中指定的路径即nginx本地路径。同时也可以进行一些缓存的设置。小结nginx的功能非常强大,还有很多需要探索,上面的一些配置都是公司配置的真实应用(精简过了),如果您有什么意见或者建议,欢迎在下方留言… ...

March 11, 2019 · 2 min · jiezi

Webpack Loader 高手进阶(一)

文章首发于个人github blog: Biu-blog,欢迎大家关注Webpack loader 详解loader 的配置Webpack 对于一个 module 所使用的 loader 对开发者提供了2种使用方式:webpack config 配置形式,形如:// webpack.config.jsmodule.exports = { … module: { rules: [{ test: /.vue$/, loader: ‘vue-loader’ }, { test: /.scss$/, use: [ ‘vue-style-loader’, ‘css-loader’, { loader: ‘sass-loader’, options: { data: ‘$color: red;’ } } ] }] } …}inline 内联形式// moduleimport a from ‘raw-loader!../../utils.js'2种不同的配置形式,在 webpack 内部有着不同的解析方式。此外,不同的配置方式也决定了最终在实际加载 module 过程中不同 loader 之间相互的执行顺序等。loader 的匹配在讲 loader 的匹配过程之前,首先从整体上了解下 loader 在整个 webpack 的 workflow 过程中出现的时机。在一个 module 构建过程中,首先根据 module 的依赖类型(例如 NormalModuleFactory)调用对应的构造函数来创建对应的模块。在创建模块的过程中(new NormalModuleFactory()),会根据开发者的 webpack.config 当中的 rules 以及 webpack 内置的 rules 规则实例化 RuleSet 匹配实例,这个 RuleSet 实例在 loader 的匹配过滤过程中非常的关键,具体的源码解析可参见Webpack Loader Ruleset 匹配规则解析。实例化 RuleSet 后,还会注册2个钩子函数:class NormalModuleFactory { … // 内部嵌套 resolver 的钩子,完成相关的解析后,创建这个 normalModule this.hooks.factory.tap(‘NormalModuleFactory’, () => (result, callback) => { … }) // 在 hooks.factory 的钩子内部进行调用,实际的作用为解析构建一共 module 所需要的 loaders 及这个 module 的相关构建信息(例如获取 module 的 packge.json等) this.hooks.resolver.tap(‘NormalModuleFactory’, () => (result, callback) => { … }) …}当 NormalModuleFactory 实例化完成后,并在 compilation 内部调用这个实例的 create 方法开始真实开始创建这个 normalModule。首先调用hooks.factory获取对应的钩子函数,接下来就调用 resolver 钩子(hooks.resolver)进入到了 resolve 的阶段,在真正开始 resolve loader 之前,首先就是需要匹配过滤找到构建这个 module 所需要使用的所有的 loaders。首先进行的是对于 inline loaders 的处理:// NormalModuleFactory.js// 是否忽略 preLoader 以及 normalLoaderconst noPreAutoLoaders = requestWithoutMatchResource.startsWith("-!");// 是否忽略 normalLoaderconst noAutoLoaders = noPreAutoLoaders || requestWithoutMatchResource.startsWith("!");// 忽略所有的 preLoader / normalLoader / postLoaderconst noPrePostAutoLoaders = requestWithoutMatchResource.startsWith("!!");// 首先解析出所需要的 loader,这种 loader 为内联的 loaderlet elements = requestWithoutMatchResource .replace(/^-?!+/, “”) .replace(/!!+/g, “!”) .split("!");let resource = elements.pop(); // 获取资源的路径elements = elements.map(identToLoaderRequest); // 获取每个loader及对应的options配置(将inline loader的写法变更为module.rule的写法)首先是根据模块的路径规则,例如模块的路径是以这些符号开头的 ! / -! / !! 来判断这个模块是否只是使用 inline loader,或者剔除掉 preLoader, postLoader 等规则:! 忽略 webpack.config 配置当中符合规则的 normalLoader-! 忽略 webpack.config 配置当中符合规则的 preLoader/normalLoader!! 忽略 webpack.config 配置当中符合规则的 postLoader/preLoader/normalLoader这几个匹配规则主要适用于在 webpack.config 已经配置了对应模块使用的 loader,但是针对一些特殊的 module,你可能需要单独的定制化的 loader 去处理,而不是走常规的配置,因此可以使用这些规则来进行处理。接下来将所有的 inline loader 转化为数组的形式,例如:import ‘style-loader!css-loader!stylus-loader?a=b!../../common.styl’最终 inline loader 统一格式输出为:[{ loader: ‘style-loader’, options: undefined}, { loader: ‘css-lodaer’, options: undefined}, { loader: ‘stylus-loader’, options: ‘?a=b’}]对于 inline loader 的处理便是直接对其进行 resolve,获取对应 loader 的相关信息:asyncLib.parallel([ callback => this.resolveRequestArray( contextInfo, context, elements, loaderResolver, callback ), callback => { // 对这个 module 进行 resolve … callack(null, { resouceResolveData, // 模块的基础信息,包含 descriptionFilePath / descriptionFileData 等(即 package.json 等信息) resource // 模块的绝对路径 }) }], (err, results) => { const loaders = results[0] // 所有内联的 loaders const resourceResolveData = results[1].resourceResolveData; // 获取模块的基本信息 resource = results[1].resource; // 模块的绝对路径 … // 接下来就要开始根据引入模块的路径开始匹配对应的 loaders let resourcePath = matchResource !== undefined ? matchResource : resource; let resourceQuery = “”; const queryIndex = resourcePath.indexOf("?"); if (queryIndex >= 0) { resourceQuery = resourcePath.substr(queryIndex); resourcePath = resourcePath.substr(0, queryIndex); } // 获取符合条件配置的 loader,具体的 ruleset 是如何匹配的请参见 ruleset 解析(https://github.com/CommanderXL/Biu-blog/issues/30) const result = this.ruleSet.exec({ resource: resourcePath, // module 的绝对路径 realResource: matchResource !== undefined ? resource.replace(/?./, “”) : resourcePath, resourceQuery, // module 路径上所带的 query 参数 issuer: contextInfo.issuer, // 所解析的 module 的发布者 compiler: contextInfo.compiler }); // result 为最终根据 module 的路径及相关匹配规则过滤后得到的 loaders,为 webpack.config 进行配置的 // 输出的数据格式为: / [{ type: ‘use’, value: { loader: ‘vue-style-loader’, options: {} }, enforce: undefined // 可选值还有 pre/post 分别为 pre-loader 和 post-loader }, { type: ‘use’, value: { loader: ‘css-loader’, options: {} }, enforce: undefined }, { type: ‘use’, value: { loader: ‘stylus-loader’, options: { data: ‘$color red’ } }, enforce: undefined }] */ const settings = {}; const useLoadersPost = []; // post loader const useLoaders = []; // normal loader const useLoadersPre = []; // pre loader for (const r of result) { if (r.type === “use”) { // postLoader if (r.enforce === “post” && !noPrePostAutoLoaders) { useLoadersPost.push(r.value); } else if ( r.enforce === “pre” && !noPreAutoLoaders && !noPrePostAutoLoaders ) { // preLoader useLoadersPre.push(r.value); } else if ( !r.enforce && !noAutoLoaders && !noPrePostAutoLoaders ) { // normal loader useLoaders.push(r.value); } } else if ( typeof r.value === “object” && r.value !== null && typeof settings[r.type] === “object” && settings[r.type] !== null ) { settings[r.type] = cachedMerge(settings[r.type], r.value); } else { settings[r.type] = r.value; } // 当获取到 webpack.config 当中配置的 loader 后,再根据 loader 的类型进行分组(enforce 配置类型) // postLoader 存储到 useLoaders 内部 // preLoader 存储到 usePreLoaders 内部 // normalLoader 存储到 useLoaders 内部 // 这些分组最终会决定加载一个 module 时不同 loader 之间的调用顺序 // 当分组过程进行完之后,即开始 loader 模块的 resolve 过程 asyncLib.parallel([ [ // resolve postLoader this.resolveRequestArray.bind( this, contextInfo, this.context, useLoadersPost, loaderResolver ), // resove normal loaders this.resolveRequestArray.bind( this, contextInfo, this.context, useLoaders, loaderResolver ), // resolve preLoader this.resolveRequestArray.bind( this, contextInfo, this.context, useLoadersPre, loaderResolver ) ], (err, results) => { … // results[0] -> postLoader // results[1] -> normalLoader // results[2] -> preLoader // 这里将构建 module 需要的所有类型的 loaders 按照一定顺序组合起来,对应于: // [postLoader, inlineLoader, normalLoader, preLoader] // 最终 loader 所执行的顺序对应为: preLoader -> normalLoader -> inlineLoader -> postLoader // 不同类型 loader 上的 pitch 方法执行的顺序为: postLoader.pitch -> inlineLoader.pitch -> normalLoader.pitch -> preLoader.pitch (具体loader内部执行的机制后文会单独讲解) loaders = results[0].concat(loaders, results[1], results[2]); process.nextTick(() => { … // 执行回调,创建 module }) } ]) }})简单总结下匹配的流程就是:首先处理 inlineLoaders,对其进行解析,获取对应的 loader 模块的信息,接下来利用 ruleset 实例上的匹配过滤方法对 webpack.config 中配置的相关 loaders 进行匹配过滤,获取构建这个 module 所需要的配置的的 loaders,并进行解析,这个过程完成后,便进行所有 loaders 的拼装工作,并传入创建 module 的回调中。文章首发于个人github blog: Biu-blog,欢迎大家关注 ...

March 10, 2019 · 4 min · jiezi

如何使用 docker 部署前端应用

如何使用 docker 部署前端应用docker 变得越来越流行,它可以轻便灵活地隔离环境,进行扩容,方便运维管理。对开发者也更方便开发,测试与部署。最重要的是, 当你面对一个陌生的项目,你可以照着 Dockerfile,甚至不看文档(文档也不一定全,全也不一定对)就可以很快让它在本地跑起来。现在很强调 devops 的理念,我把 devops 五个大字放在电脑桌面上,格物致知了一天。豁然开朗,devops 的意思就是写一个 Dockerfile 去跑应用(开玩笑。这里介绍如何使用 Docker 部署前端应用。千里之行,始于足下,足下的意思就是,先让它能够跑起来。本文地址 http://shanyue.tech/post/depl…先让它跑起来首先,简单介绍一下一个典型的前端应用部署流程npm install, 安装依赖npm run build,编译,打包,生成静态资源服务化静态资源介绍完部署流程后,简单写一个 DockerfileFROM node:alpine# 代表生产环境ENV PROJECT_ENV productionWORKDIR /codeADD . /codeRUN npm install && npm run build && npm install -g http-serverEXPOSE 80CMD http-server ./public -p 80现在这个前端服务已经跑起来了。接下来你可以完成部署的其它阶段了。一般情况下,以下就成了运维的工作了,不过,拓展自己的知识边界总是没错的。使用 nginx 或者 traefik 做反向代理使用 kubernetes 或者 compose 等做编排。使用 gitlab ci 或者 drone ci 等做 CI/CD这时镜像存在有两个问题,导致每次部署时间过长,不利于产品的快速交付构建镜像时间过长构建镜像大小过大,1G+从 dependencies 和 devDependencies 下手陆小凤说过,一个前端程序员若是每天工作八个小时,至少有两个小时是白白浪费了的。一个小时用来 npm install,另一个小时用来 npm run build。对于每次部署,如果能够减少无用包的下载,便能够节省很多镜像构建时间。eslint,mocha,chai 等代码风格测试模块可以放到 devDependencies 中。在生产环境中使用 npm install –production 装包。关于两者的区别可以参考文档 https://docs.npmjs.com/files/...FROM node:alpineENV PROJECT_ENV productionWORKDIR /codeADD . /codeRUN npm install –production && npm run build && npm install -g http-serverEXPOSE 80CMD http-server ./public -p 80好像是快了那么一点点。我们注意到,相对于项目的源文件来讲,package.json 是相对稳定的。如果没有新的安装包需要下载,则再次构建镜像时,无需重新装包。则可以在 npm install 上节省一半的时间。利用镜像缓存对于 ADD 来讲,如果需要添加的内容没有发生变化,则可以利用缓存。把 package.json 与源文件分隔开写入镜像是一个很好的选择。目前,如果没有新的安装包更新的话,可以节省一半时间FROM node:alpineENV PROJECT_ENV production# http-server 不变动也可以利用缓存RUN npm install -g http-serverWORKDIR /codeADD package.json /codeRUN npm install –productionADD . /codeRUN npm run buildEXPOSE 80CMD http-server ./public -p 80关于利用缓存有更多细节,需要特别注意一下,如 RUN git clone <repo> 的缓存此类参考官方文档 https://docs.docker.com/devel…多阶段构建得益于缓存,现在镜像构建时间已经快了不少。但是,镜像的体积依旧过于庞大,也会增加每次的部署时间考虑下每次 CI 部署的流程在构建服务器构建镜像把镜像推至镜像仓库服务器,在生产服务器拉取镜像,启动容器显而易见,镜像体积过大造成传输效率低下,增加每次部署的延时。即使,构建服务器与生产服务器在同一节点下,没有延时的问题。减少镜像体积也能够节省磁盘空间关于镜像体积的过大,很大一部分是因为node_modules 臭名昭著的体积但最后我们只需要 public 文件夹下的内容,对于源文件以及node_modules下文件,占用体积过大且不必要,造成浪费。此时可以利用 Docker 的多阶段构建,仅来提取编译后文件参考官方文档 https://docs.docker.com/devel...FROM node:alpine as builderENV PROJECT_ENV production# http-server 不变动也可以利用缓存WORKDIR /codeADD package.json /codeRUN npm install –productionADD . /codeRUN npm run build# 选择更小体积的基础镜像FROM nginx:alpineCOPY –from=builder /code/public /usr/share/nginx/html此时,镜像体积从 1G+ 变成了 50M+使用 CDN分析一下 50M+ 的镜像体积,nginx:alpine 的镜像是16M,剩下的40M是静态资源。如果把静态资源给上传到 CDN,则没有必要打入镜像了,此时镜像大小会控制在 20M 以下关于静态资源,可以分类成两部分/static,此类文件在项目中直接引用根路径,打包时复制进 /public 下,需要被打入镜像/build,此类文件需要 require 使用,会被 webpack 打包并加 hash 值,并可以通过 publicPath 修改资源地址。可以把此类文件上传至 cdn,并加上永久缓存,不需要打入镜像FROM node:alpine as builderENV PROJECT_ENV production# http-server 不变动也可以利用缓存WORKDIR /codeADD package.json /codeRUN npm install –productionADD . /codeRUN npm run build# 选择更小体积的基础镜像FROM nginx:alpineCOPY –from=builder code/public/index.html code/public/favicon.ico /usr/share/nginx/html/COPY –from=builder code/public/static /usr/share/nginx/html/static ...

March 9, 2019 · 2 min · jiezi

Web前端面试技术点

常规问题:一般来说会问如下几方面的问题:做过最满意的项目是什么?项目背景为什么要做这件事情?最终达到什么效果?你处于什么样的角色,起到了什么方面的作用?在项目中遇到什么技术问题?具体是如何解决的?如果再做这个项目,你会在哪些方面进行改善?技术二面主要判断技术深度及广度你最擅长的技术是什么?你觉得你在这个技术上的水平到什么程度了?你觉得最高级别应该是怎样的?浏览器及性能一个页面从输入 URL 到页面加载完的过程中都发生了什么事情?越详细越好 (这个问既考察技术深度又考察技术广度,其实要答好是相当难的,注意越详细越好)谈一下你所知道的页面性能优化方法?这些优化方法背后的原理是什么?除了这些常规的,你还了解什么最新的方法么?如何分析页面性能?其它除了前端以外还了解什么其它技术么?对计算机基础的了解情况,比如常见数据结构、编译原理等技术点沟通:HTML+CSS1、盒子模型,块级元素和行内元素特性与区别。 2、行内块的使用,兼容性解决。 3、清除浮动的方式以及各自的优劣。4、文档流的概念、定位的理解以及z-index计算规则&浏览器差异性。 5、CSS选择器以及优先级计算。 6、常用的CSS hack。7、遇到的兼容性问题与解决方法。 8、垂直水平居中的实现方式。9、常用布局的实现(两列布局、三列适应布局,两列等高适应布局等)。Javascript1、犀牛书封面的犀牛属于神马品种?(蛋逼活跃气氛用。。。) 2、常用的浏览器内核。 3、常用的DOM操作,新建、添加、删除、移动、查找等。4、String于Array常用方法。 5、设备与平台监测。 6、DOM的默认事件、事件模型、事件委托、阻止默认事件、冒泡事件的方式等。7、jQuery的bind、live、on、delegate的区别(考察点与上一条重叠,切入点不同)。8、JS变量提升、匿名函数、原型继承、作用域、闭包机制等。 9、对HTTP协议的理解。 10、Ajax的常用操作,JS跨域的实现原理。HTML:语义标签语义化CSS:动态居中动画Bootstrap 样式类Preprocessor兼容性 Hack与特征检测CSS3属性与性能JS:Name hoistingPrototypeClosureMain loopPromiseDelegationCross domainMobile:渐进增强移动端交互兼容性问题Debug工具 方法主体是看简历发挥,对方写什么就问什么为主:框架、库、浏览器工作原理、NLP、算法、HTTP… 辅助问题几乎是我个人必备的问题:为什么做前端,学习前端过程。1、跟什么人在一起工作 2、过去项目的挑战 3、自学的途径3个问题基本上就知道这个人的能力水平和瓶颈了,人的很多局限都是被环境限制的,通过闲聊中夹杂的不经意的问题,候选人的画像就已经很鲜明了。处于当前的环境多长时间,有没有突破环境限制的行动,就能评估出潜力和眼界。什么浏览器兼容、作用域、框架等等的东西不会,不记得都可以学,要不了多长时间,关键还是有没有潜力、有没有好的习惯。在能力方面:对 HTML / CSS / JavaScript 具有专家级别的知识;有较熟练使用 AngularJS / Ember.js / jQuery 或者其它类库的经验;较熟悉第三方组件(插件)生态环境及具体案例;有较熟练使用 Jade / Swig / Handlebars / Mustache 或者其它模板引擎的经验;有较熟练使用 SASS 或者其它 CSS 预处理器的经验;有较熟练使用 CoffeeScript 的经验;对 CSS / JavaScript 设计模式有很好的认识及应用;对常用数据结构和算法熟悉;有使用 GruntJS / GulpJS 任务运行器的经验;有使用 Yeoman 生成器的经验;有诸如 Bower / Volo / JSPM 等前端静态资源包管理器使用经验;熟悉本地及远程(甄姬)调试操作;有 Git 的使用经验;Q:简单介绍下 React / Vue 的生命周期A:几个钩子函数基本能报出来(如果不讲究按顺序、按挂载/更新区分、能把单词用英文念出来并且念对的话),稍微深入一点问下各个阶段都做了什么,一半以上就“不太清楚”了。更有甚者我问React,对方回答 created、mounted,提醒之后还觉得自己没错的。Q:【React】定义一个组件时候,如何决定要用 Functional 还是 Class?A:简单的用 Function,复杂的用 Class。(不能算错吧……但也不能算答到点子上)追问怎么界定“复杂”,答不上来。Q:【React】HOC、(非)受控组件、shouldComponentUpdate、React 16 的变化A:不清楚、没接触过。Q:【Vue】介绍一下计算属性,和 data、methods、watch 的异同A:基本都能巴拉一些,说的大部分都对,但就是说不到最关键的“当且仅当计算属性依赖的 data 改变时才会自动计算”。Q:【Vue】为什么 SFC 里的 data 必须是一个函数返回的对象,而不能就只是一个对象?A:我承认这个问题有点小难,有一定的区分度,不是每个人都有关注过,但是官方文档有说明这一点,但凡看过的肯定有印象。即便没完整看过文档,在初次学习的过程中难道就不觉得奇怪吗?“学而不思”的人和“学而思”的人,区别还是挺大的。Q:CSS 选择器的权重A:经典问题了吧?背都能背出来吧?伪类、伪元素分不清楚,只知道内联、!important、ID、Class之间的顺序,加上其它的就懵了,而且只说谁大于谁,讲不出具体的计算方法。单层选择器比较还行,几个叠加起来就迷糊了。Q:JS 有哪几种原始类型A:基础题,能说上来几个,答不全,主要问题集中在 null 和 undefined 没考虑进去、对象和数组算不算原始类型、以及 Symbol很多人不知道。Q:ES 2015+ 有哪些新特性A:这题可以说的很多,根据应聘者的回答去展开,可以很容易地看出应聘者有没有系统地学习过这方面的东西,以及有没有持续地去跟进语言标准的发展。但这一题能回答的比较好的,寥寥无几,大部分是遇到问题然后零零散散现学的,不够全面、也不够深入,简单用过,但稍微问点细节就只有“尴尬而不失礼仪的微笑”了。Q:工程化工具的使用(Webpack、ESLint、Yarn、Git、……)A:基本都有所接触,但只是“用过”,算不上“会用”,一切顺利还好,真遇到问题了,立马就懵。Q:Node.jsA:写过 Demo 的水平。(比较初级)Q:未来 1~2 年的职业规划、下一步最想学的技术、最希望往什么方向发展、怎么看待XXX技术A:大部分人对自己没有一个明确的态度和规划。说白了就是还没从学校里出来,什么都等着别人来安排。通用技能有哪些(请看如下图)? ...

March 9, 2019 · 1 min · jiezi

前端静态资源自动化处理版本号防缓存

前端静态资源自动化处理版本号防缓存浏览器会默认缓存网站的静态资源文件,如:js文件、css文件、图片等。缓存带来网站性能提升的同时也带来了一些困扰,最常见的问题就是不能及时更新静态资源,造成新版本发布时用户无法及时看到新版本的变化,严重影响了用户体验。上述问题,最简单的办法就是在资源的请求路径上添加版本号,格式如下:url?v=1.0.0每次在更改资源的时候,手动修改版本号,但是每次手动改那么多后缀有些费事,现在有很多的工具可以让我们更轻松的完成这项工具。本文将探讨使用目前最流行的前端构建工具 Gulp 和 Webpack 自动化为静态资源添加版本号防缓存处理。使用 Gulp 处理文件版本Gulp 是一个简单易用的前端自动化构建工具,非常适合于构建多页面的工作流程。安装 Gulp(这里使用的是 Gulp 4+ 版本):$ npm install –save-dev gulp安装 gulp-rev 插件:$ npm install –save-dev gulp-revgulp-rev 插件的作用就是为静态资源添加版本号。新建 gulpfile.js 文件:const gulp = require(‘gulp’);const rev = require(‘gulp-rev’);// 添加版本号gulp.task(‘rev’, () => { return gulp.src(‘src/css/.css’) .pipe(rev()) // 将所有匹配到的文件名全部生成相应的版本号 .pipe(gulp.dest(‘dist/css’)) .pipe(rev.manifest()) //把所有生成的带版本号的文件名保存到rev-manifest.json文件中 .pipe(gulp.dest(‘rev/css’)) //把rev-manifest.json文件保存到指定的路径});执行 rev 任务后,rev/css 文件加下多了一个 rev-manifest.json 文件。rev-manifest.json 文件的内容如下:{ “index.css”: “index-35c63c1fbe.css”}然后,安装 gulp-rev-collector 插件:$ npm install –save-dev gulp-rev-collectorgulp-rev-collector 插件主要是配合 gulp-rev 替换文件版本号。修改 gulpfile.js 文件:const gulp = require(‘gulp’);const rev = require(‘gulp-rev’);// 添加版本号gulp.task(‘rev’, () => { return gulp.src(‘src/css/.css’) .pipe(rev()) // 将所有匹配到的文件名全部生成相应的版本号 .pipe(gulp.dest(‘dist/css’)) .pipe(rev.manifest()) //把所有生成的带版本号的文件名保存到rev-manifest.json文件中 .pipe(gulp.dest(‘rev/css’)) //把rev-manifest.json文件保存到指定的路径});const revCollector = require(‘gulp-rev-collector’);// 控制文件版本号gulp.task(‘rev-collector’, () => { return gulp.src([‘rev//*.json’, ‘src//*.html’]) .pipe(revCollector({ replaceReved: true })) .pipe(gulp.dest(‘dist’))})gulp.task(‘default’, gulp.series(‘clean’, ‘rev’, ‘rev-collector’))执行 gulp 默认任务。检查 dist 下 index.html 文件 css 的版本是否替换成功。使用 Webpack 处理文件版本Webpack 是一个现代 JavaScript 应用程序的静态模块打包器,非常适合于构建单页面的工作流程,当然也可以构建多页面的工作流程。安装 Webpack(这里使用的是 webpack 4+ 版本)。$ npm install –save-dev webpack webpack-cli通过使用 output.filename 进行文件名替换,webpack 使用 [chunkhash] 替换文件名,在文件名中包含一个 chunk 相关(chunk-specific)的哈希。安装 clean-webpack-plugin 插件(清理文件夹):$ npm install –save-dev clean-webpack-pluginclean-webpack-plugin 插件的作用是清理文件夹,由于每次打包的文件版本不同,输出目录会生成很多不同版本的目标文件,所以需要清理文件夹。配置文件 webpack.config.js 如下:const path = require(‘path’);const CleanWebpackPlugin = require(‘clean-webpack-plugin’);module.exports = { entry: ‘./src/index.js’, output: { filename: ‘bundle.[chunkhash:5].js’, //这里设置 [chunkhash] 替换文件名,数字5为 chunkhash 的字符长度。 path: path.resolve(__dirname, ‘dist’) }, plugins: [ new CleanWebpackPlugin([‘dist’]) ]}在 src 目录新建一个 index.html 文件:<!DOCTYPE html><html> <head> <meta charset=“utf-8”> <title>Webpack实现静态资源版本管理自动化</title> </head> <body> <script src=“index.js”></script> </body></html>安装 html-webpack-plugin 插件:$ npm install –save-dev html-webpack-pluginhtml-webpack-plugin 插件编译 html 替换带有哈希值版本信息的资源文件。修改 webpack.config.js 文件:const path = require(‘path’);const CleanWebpackPlugin = require(‘clean-webpack-plugin’);const HtmlWebpackPlugin = require(‘html-webpack-plugin’);module.exports = { entry: ‘./src/index.js’, output: { filename: ‘bundle.[chunkhash:5].js’, //这里设置 [chunkhash] 替换文件名,数字5为 chunkhash 的字符长度。 path: path.resolve(__dirname, ‘dist’) }, plugins: [ new CleanWebpackPlugin([‘dist’]), new HtmlWebpackPlugin({ title: ‘Webpack实现静态资源版本管理自动化’ }) ]}html-webpack-plugin 默认入口文件为 index.html,具体的参数配置请参考https://www.npmjs.com/package/html-webpack-plugin。关于 Webpack 处理缓存的更多教程请移步官方文档。符录usuallyjs函数库: https://github.com/JofunLiang/usuallyjs ...

March 5, 2019 · 2 min · jiezi

webpack系列-插件机制杂记

系列文章Webpack系列-第一篇基础杂记 webpack系列-插件机制杂记前言webpack本身并不难,他所完成的各种复杂炫酷的功能都依赖于他的插件机制。或许我们在日常的开发需求中并不需要自己动手写一个插件,然而,了解其中的机制也是一种学习的方向,当插件出现问题时,我们也能够自己来定位。TapableWebpack的插件机制依赖于一个核心的库, Tapable。 在深入webpack的插件机制之前,需要对该核心库有一定的了解。Tapable是什么tapable 是一个类似于nodejs 的EventEmitter 的库, 主要是控制钩子函数的发布与订阅。当然,tapable提供的hook机制比较全面,分为同步和异步两个大类(异步中又区分异步并行和异步串行),而根据事件执行的终止条件的不同,由衍生出 Bail/Waterfall/Loop 类型。Tapable的使用 (该小段内容引用文章)基本使用:const { SyncHook} = require(’tapable’)// 创建一个同步 Hook,指定参数const hook = new SyncHook([‘arg1’, ‘arg2’])// 注册hook.tap(‘a’, function (arg1, arg2) { console.log(‘a’)})hook.tap(‘b’, function (arg1, arg2) { console.log(‘b’)})hook.call(1, 2)钩子类型:BasicHook:执行每一个,不关心函数的返回值,有SyncHook、AsyncParallelHook、AsyncSeriesHook。 BailHook:顺序执行 Hook,遇到第一个结果result!==undefined则返回,不再继续执行。有:SyncBailHook、AsyncSeriseBailHook, AsyncParallelBailHook。 什么样的场景下会使用到 BailHook 呢?设想如下一个例子:假设我们有一个模块 M,如果它满足 A 或者 B 或者 C 三者任何一个条件,就将其打包为一个单独的。这里的 A、B、C 不存在先后顺序,那么就可以使用 AsyncParallelBailHook 来解决:x.hooks.拆分模块的Hook.tap(‘A’, () => { if (A 判断条件满足) { return true } }) x.hooks.拆分模块的Hook.tap(‘B’, () => { if (B 判断条件满足) { return true } }) x.hooks.拆分模块的Hook.tap(‘C’, () => { if (C 判断条件满足) { return true } })如果 A 中返回为 true,那么就无须再去判断 B 和 C。但是当 A、B、C 的校验,需要严格遵循先后顺序时,就需要使用有顺序的 SyncBailHook(A、B、C 是同步函数时使用) 或者 AsyncSeriseBailHook(A、B、C 是异步函数时使用)。WaterfallHook:类似于 reduce,如果前一个 Hook 函数的结果 result !== undefined,则 result 会作为后一个 Hook 函数的第一个参数。既然是顺序执行,那么就只有 Sync 和 AsyncSeries 类中提供这个Hook:SyncWaterfallHook,AsyncSeriesWaterfallHook当一个数据,需要经过 A,B,C 三个阶段的处理得到最终结果,并且 A 中如果满足条件 a 就处理,否则不处理,B 和 C 同样,那么可以使用如下x.hooks.tap(‘A’, (data) => { if (满足 A 需要处理的条件) { // 处理数据 data return data } else { return } })x.hooks.tap(‘B’, (data) => { if (满足B需要处理的条件) { // 处理数据 data return data } else { return } }) x.hooks.tap(‘C’, (data) => { if (满足 C 需要处理的条件) { // 处理数据 data return data } else { return } })LoopHook:不停的循环执行 Hook,直到所有函数结果 result === undefined。同样的,由于对串行性有依赖,所以只有 SyncLoopHook 和 AsyncSeriseLoopHook (PS:暂时没看到具体使用 Case)Tapable的源码分析Tapable 基本逻辑是,先通过类实例的 tap 方法注册对应 Hook 的处理函数, 这里直接分析sync同步钩子的主要流程,其他的异步钩子和拦截器等就不赘述了。const hook = new SyncHook([‘arg1’, ‘arg2’])从该句代码, 作为源码分析的入口,class SyncHook extends Hook { // 错误处理,防止调用者调用异步钩子 tapAsync() { throw new Error(“tapAsync is not supported on a SyncHook”); } // 错误处理,防止调用者调用promise钩子 tapPromise() { throw new Error(“tapPromise is not supported on a SyncHook”); } // 核心实现 compile(options) { factory.setup(this, options); return factory.create(options); }}从类SyncHook看到, 他是继承于一个基类Hook, 他的核心实现compile等会再讲, 我们先看看基类Hook// 变量的初始化constructor(args) { if (!Array.isArray(args)) args = []; this._args = args; this.taps = []; this.interceptors = []; this.call = this._call; this.promise = this._promise; this.callAsync = this._callAsync; this._x = undefined;}初始化完成后, 通常会注册一个事件, 如:// 注册hook.tap(‘a’, function (arg1, arg2) { console.log(‘a’)})hook.tap(‘b’, function (arg1, arg2) { console.log(‘b’)})很明显, 这两个语句都会调用基类中的tap方法:tap(options, fn) { // 参数处理 if (typeof options === “string”) options = { name: options }; if (typeof options !== “object” || options === null) throw new Error( “Invalid arguments to tap(options: Object, fn: function)” ); options = Object.assign({ type: “sync”, fn: fn }, options); if (typeof options.name !== “string” || options.name === “”) throw new Error(“Missing name for tap”); // 执行拦截器的register函数, 比较简单不分析 options = this._runRegisterInterceptors(options); // 处理注册事件 this._insert(options);}从上面的源码分析, 可以看到_insert方法是注册阶段的关键函数, 直接进入该方法内部_insert(item) { // 重置所有的 调用 方法 this._resetCompilation(); // 将注册事件排序后放进taps数组 let before; if (typeof item.before === “string”) before = new Set([item.before]); else if (Array.isArray(item.before)) { before = new Set(item.before); } let stage = 0; if (typeof item.stage === “number”) stage = item.stage; let i = this.taps.length; while (i > 0) { i–; const x = this.taps[i]; this.taps[i + 1] = x; const xStage = x.stage || 0; if (before) { if (before.has(x.name)) { before.delete(x.name); continue; } if (before.size > 0) { continue; } } if (xStage > stage) { continue; } i++; break; } this.taps[i] = item;}}_insert主要是排序tap并放入到taps数组里面, 排序的算法并不是特别复杂,这里就不赘述了, 到了这里, 注册阶段就已经结束了, 继续看触发阶段。hook.call(1, 2) // 触发函数在基类hook中, 有一个初始化过程,this.call = this._call; Object.defineProperties(Hook.prototype, { _call: { value: createCompileDelegate(“call”, “sync”), configurable: true, writable: true }, _promise: { value: createCompileDelegate(“promise”, “promise”), configurable: true, writable: true }, _callAsync: { value: createCompileDelegate(“callAsync”, “async”), configurable: true, writable: true }});我们可以看出_call是由createCompileDelegate生成的, 往下看function createCompileDelegate(name, type) { return function lazyCompileHook(…args) { this[name] = this._createCall(type); return thisname; };}createCompileDelegate返回一个名为lazyCompileHook的函数,顾名思义,即懒编译, 直到调用call的时候, 才会编译出正在的call函数。 createCompileDelegate也是调用的_createCall, 而_createCall调用了Compier函数_createCall(type) { return this.compile({ taps: this.taps, interceptors: this.interceptors, args: this._args, type: type });} compile(options) { throw new Error(“Abstract: should be overriden”);}可以看到compiler必须由子类重写, 返回到syncHook的compile函数, 即我们一开始说的核心方法class SyncHookCodeFactory extends HookCodeFactory { content({ onError, onResult, onDone, rethrowIfPossible }) { return this.callTapsSeries({ onError: (i, err) => onError(err), onDone, rethrowIfPossible }); }}const factory = new SyncHookCodeFactory();class SyncHook extends Hook { … compile(options) { factory.setup(this, options); return factory.create(options); }}关键就在于SyncHookCodeFactory和工厂类HookCodeFactory, 先看setup函数,setup(instance, options) { // 这里的instance 是syncHook 实例, 其实就是把tap进来的钩子数组给到钩子的_x属性里. instance._x = options.taps.map(t => t.fn);}然后是最关键的create函数, 可以看到最后返回的fn,其实是一个new Function动态生成的函数create(options) { // 初始化参数,保存options到本对象this.options,保存new Hook([“options”]) 传入的参数到 this._args this.init(options); let fn; // 动态构建钩子,这里是抽象层,分同步, 异步, promise switch (this.options.type) { // 先看同步 case “sync”: // 动态返回一个钩子函数 fn = new Function( // 生成函数的参数,no before no after 返回参数字符串 xxx,xxx 在 // 注意这里this.args返回的是一个字符串, // 在这个例子中是options this.args(), ‘“use strict”;\n’ + this.header() + this.content({ onError: err => throw ${err};\n, onResult: result => return ${result};\n, onDone: () => “”, rethrowIfPossible: true }) ); break; case “async”: fn = new Function( this.args({ after: “_callback” }), ‘“use strict”;\n’ + this.header() + // 这个 content 调用的是子类类的 content 函数, // 参数由子类传,实际返回的是 this.callTapsSeries() 返回的类容 this.content({ onError: err => _callback(${err});\n, onResult: result => _callback(null, ${result});\n, onDone: () => “_callback();\n” }) ); break; case “promise”: let code = “”; code += ‘“use strict”;\n’; code += “return new Promise((_resolve, _reject) => {\n”; code += “var _sync = true;\n”; code += this.header(); code += this.content({ onError: err => { let code = “”; code += “if(_sync)\n”; code += _resolve(Promise.resolve().then(() =&gt; { throw ${err}; }));\n; code += “else\n”; code += _reject(${err});\n; return code; }, onResult: result => _resolve(${result});\n, onDone: () => “_resolve();\n” }); code += “_sync = false;\n”; code += “});\n”; fn = new Function(this.args(), code); break; } // 把刚才init赋的值初始化为undefined // this.options = undefined; // this._args = undefined; this.deinit(); return fn;}最后生成的代码大致如下, 参考文章"use strict";function (options) { var _context; var _x = this._x; var _taps = this.taps; var _interterceptors = this.interceptors;// 我们只有一个拦截器所以下面的只会生成一个 _interceptors[0].call(options); var _tap0 = _taps[0]; _interceptors[0].tap(_tap0); var _fn0 = _x[0]; _fn0(options); var _tap1 = _taps[1]; _interceptors[1].tap(_tap1); var _fn1 = _x[1]; _fn1(options); var _tap2 = _taps[2]; _interceptors[2].tap(_tap2); var _fn2 = _x[2]; _fn2(options); var _tap3 = _taps[3]; _interceptors[3].tap(_tap3); var _fn3 = _x[3]; _fn3(options);}ok, 以上就是Tapabled的机制, 然而本篇的主要对象其实是基于tapable实现的compile和compilation对象。不过由于他们都是基于tapable,所以介绍的篇幅相对短一点。compilecompile是什么compiler 对象代表了完整的 webpack 环境配置。这个对象在启动 webpack 时被一次性建立,并配置好所有可操作的设置,包括 options,loader 和 plugin。当在 webpack 环境中应用一个插件时,插件将收到此 compiler 对象的引用。可以使用 compiler 来访问 webpack 的主环境。也就是说, compile是webpack的整体环境。compile的内部实现class Compiler extends Tapable { constructor(context) { super(); this.hooks = { /** @type {SyncBailHook<Compilation>} / shouldEmit: new SyncBailHook([“compilation”]), /* @type {AsyncSeriesHook<Stats>} / done: new AsyncSeriesHook([“stats”]), /* @type {AsyncSeriesHook<>} / additionalPass: new AsyncSeriesHook([]), /* @type {AsyncSeriesHook<Compiler>} / …… …… some code }; …… …… some code}可以看到, Compier继承了Tapable, 并且在实例上绑定了一个hook对象, 使得Compier的实例compier可以像这样使用compiler.hooks.compile.tapAsync( ‘afterCompile’, (compilation, callback) => { console.log(‘This is an example plugin!’); console.log(‘Here’s the compilation object which represents a single build of assets:’, compilation); // 使用 webpack 提供的 plugin API 操作构建结果 compilation.addModule(/ … /); callback(); });compilation什么是compilationcompilation 对象代表了一次资源版本构建。当运行 webpack 开发环境中间件时,每当检测到一个文件变化,就会创建一个新的 compilation,从而生成一组新的编译资源。一个 compilation 对象表现了当前的模块资源、编译生成资源、变化的文件、以及被跟踪依赖的状态信息。compilation 对象也提供了很多关键时机的回调,以供插件做自定义处理时选择使用。compilation的实现class Compilation extends Tapable { /* * Creates an instance of Compilation. * @param {Compiler} compiler the compiler which created the compilation / constructor(compiler) { super(); this.hooks = { /* @type {SyncHook<Module>} / buildModule: new SyncHook([“module”]), /* @type {SyncHook<Module>} / rebuildModule: new SyncHook([“module”]), /* @type {SyncHook<Module, Error>} / failedModule: new SyncHook([“module”, “error”]), /* @type {SyncHook<Module>} / succeedModule: new SyncHook([“module”]), /* @type {SyncHook<Dependency, string>} / addEntry: new SyncHook([“entry”, “name”]), /* @type {SyncHook<Dependency, string, Error>} / } }}具体参考上面提到的compiler实现。编写一个插件了解到tapablecompilercompilation之后, 再来看插件的实现就不再一头雾水了 以下代码源自官方文档class MyExampleWebpackPlugin { // 定义 apply 方法 apply(compiler) { // 指定要追加的事件钩子函数 compiler.hooks.compile.tapAsync( ‘afterCompile’, (compilation, callback) => { console.log(‘This is an example plugin!’); console.log(‘Here’s the compilation object which represents a single build of assets:’, compilation); // 使用 webpack 提供的 plugin API 操作构建结果 compilation.addModule(/ … */); callback(); } ); }}可以看到其实就是在apply中传入一个Compiler实例, 然后基于该实例注册事件, compilation同理, 最后webpack会在各流程执行call方法。compiler和compilation一些比较重要的事件钩子compier事件钩子触发时机参数类型entry-option初始化 option-SyncBailHookrun开始编译compilerAsyncSeriesHookcompile真正开始的编译,在创建 compilation 对象之前compilationSyncHookcompilation生成好了 compilation 对象,可以操作这个对象啦compilationSyncHookmake从 entry 开始递归分析依赖,准备对每个模块进行 buildcompilationAsyncParallelHookafter-compile编译 build 过程结束compilationAsyncSeriesHookemit在将内存中 assets 内容写到磁盘文件夹之前compilationAsyncSeriesHookafter-emit在将内存中 assets 内容写到磁盘文件夹之后compilationAsyncSeriesHookdone完成所有的编译过程statsAsyncSeriesHookfailed编译失败的时候errorSyncHookcompilation事件钩子触发时机参数类型normal-module-loader普通模块 loader,真正(一个接一个地)加载模块图(graph)中所有模块的函数。loaderContext moduleSyncHookseal编译(compilation)停止接收新模块时触发。-SyncHookoptimize优化阶段开始时触发。-SyncHookoptimize-modules模块的优化modulesSyncBailHookoptimize-chunks优化 chunkchunksSyncBailHookadditional-assets为编译(compilation)创建附加资源(asset)。-AsyncSeriesHookoptimize-chunk-assets优化所有 chunk 资源(asset)。chunksAsyncSeriesHookoptimize-assets优化存储在 compilation.assets 中的所有资源(asset)assetsAsyncSeriesHook总结插件机制并不复杂,webpack也不复杂,复杂的是插件本身.. 另外, 本应该先写流程的, 流程只能后面补上了。引用不满足于只会使用系列: tapable webpack系列之二Tapable 编写一个插件 Compiler Compilation compiler和comnpilation钩子 看清楚真正的 Webpack 插件 ...

March 4, 2019 · 6 min · jiezi

百度地图 osm地图 leaflet echarts webapck的组合使用时的踩坑记录

webpack+百度地图创建 script标签进行加载export function MP(ak){ return new Promise(function (resolve, reject){ window.onload = function () { resolve(BMap) } var script = document.createElement(“script”); script.type = “text/javascript”; script.src = “http://api.map.baidu.com/api?v=2.0&ak="+ak+"&callback=init"; script.onerror = reject; document.head.appendChild(script); }); }使用:import {MP} from ‘./map.js’; MP(“your ak”).then(BMap => { // do something})webpack+百度地图+echart需要1 百度地图2 echart3 bmap.min.js 添加扩展,用于让百度地图支持echart https://github.com/apache/inc...webpack+osm地图+leaflet可能会遇见两个问题:1 地图图片错位 忘记加载leaflet.css2 webpack 中使用leaflet 的一个主要问题是默认图标的加载问题,详见https://segmentfault.com/q/10…另外也可以考虑使用动态创建<script>标签的方法,类似百度地图加载webpack+百度地图+leaflet因为leaflet本身支持的是WGS84的坐标系 ,而百度地图在中国使用的是百度坐标系,所以如果要在百度地图中使用leaflet的话,一是需要绘图数据变更为百度地图的BD09坐标系,二是需要对leaflet添加扩展,使其在进行经纬度坐标转化时使用百度地图的映射系统解决方案: http://tiven.wang/articles/us…以上问题的代码示例 https://gitlab.com/dahou/maps

February 27, 2019 · 1 min · jiezi

工作中总结前端开发流程--vue项目

开发流程需求 -> 原型 -> 开发 -> 测试 -> 上线开发1.版本控制选用git进行版本控制。新建分支进行开发,master主线,code review后进行合并。利用分支,部署不同的上线版本2.技术选型 * 根据业务需求,选择合适的技术 – vue-cli* 制定统一编码规范,便于团队协作和代码维护,例如eslint, tslint3.环境配置初始化项目完成后,提交代码到远程库。为保持环境统一,推荐以下方式:需团队共享的 npm config 配置项使用 npm: >=5.1 版本, 保持 package-lock.json 文件默认开启配置提交 package.json, package-lock.json。package.json中,项目依赖semver为^X.Y.Z项目成员首次 clone 项目代码后,执行npm install 安装依赖包4.构建优化根据实际项目,更改webpack配置。5.目录结构模块化采用模块化的方式组织代码:JS 模块化:AMD、CommonJS、UMD、ES6 ModuleCSS 模块化:less、sass、stylus、postCSS、css module组件化采用组件化的编程思想,处理 UI 层静态资源管理1.非模块化资源2.模块化资源–与模块一起进行统一管理开发结束后,一般也会经历以下几个过程:1.需求变更,重新开发2.code review3.提交测试,修改bug单,回归测试测试1.SIT测试环境测试环境,前后端分离,后台CORS,前台通过代理跨域。最好采用source map方式,利于追踪bug。一般测试通过,bug单清零,会转UAT测试。2.UAT测试环境用户验收测试,一般通过后,就准备部署上线。部署webpack进行打包后,丢到服务器上,项目上线。当然,上线前,要进行性能优化,例如配置缓存,静态资源CDN部署。

February 25, 2019 · 1 min · jiezi

Webpack系列-第一篇基础杂记

前言公司的前端项目基本都是用Webpack来做工程化的,而Webpack虽然只是一个工具,但内部涉及到非常多的知识,之前一直靠CV来解决问题,之知其然不知其所以然,希望这次能整理一下相关的知识点。简介这是webpack官方的首页图 本质上,webpack 是一个现代 JavaScript 应用程序的静态模块打包器(module bundler)。当 webpack 处理应用程序时,它会递归地构建一个依赖关系图(dependency graph),其中包含应用程序需要的每个模块,然后将所有这些模块打包成一个或多个 bundle。那么打个比方就是我们搭建一个项目好比搭建一个房子,我们把所需要的材料(js文件、图片等)交给webpack,最后webpack会帮我们做好一切,并把房子(即bundle)输出。 webpack中有几个概念需要记住entry(入口)入口起点(entry point)即是webpack通过该起点找到本次项目所直接或间接依赖的资源(模块、库等),并对其进行处理,最后输出到bundle中。入口文件由用户自定义,可以是一个或者多个,每一个entry最后对应一个bundle。output(出口)通过配置output属性可以告诉webpack将bundle命名并输出到对应的位置。loaderwebpack核心,webpack本身只能识别js文件,对于非js文件,即需要loader转换为js文件。换句话说,,Loader就是资源转换器。由于在webpack里,所有的资源都是模块,不同资源都最终转化成js去处理。针对不同形式的资源采用不同的Loader去编译,这就是Loader的意义。插件(plugin)webpack核心,loader处理非js文件,那么插件可以有更广泛的用途。整个webpack其实就是各类的插件形成的,插件的范围包括,从打包优化和压缩,一直到重新定义环境中的变量。插件接口功能极其强大,可以用来处理各种各样的任务。Chunk被entry所依赖的额外的代码块,同样可以包含一个或者多个文件。chunk也就是一个个的js文件,在异步加载中用处很大。chunk实际上就是webpack打包后的产物,如果你不想最后生成一个包含所有的bundle,那么可以生成一个个chunk,并通过按需加载引入。同时它还能通过插件提取公共依赖生成公共chunk,避免多个bundle中有多个相同的依赖代码。配置webpack的相关配置语法官方文档比较详细,这里就不赘述了。 指南 配置实践&优化url-loader & image-webpack-loaderurl-loader 可以在文件大小(单位 byte)低于指定的限制,将文件转换为DataURL,这在实际开发中非常有效,能够减少请求数,在vue-cli和create-react-app中也都能看到对这个loader的使用。// “url” loader works just like “file” loader but it also embeds // assets smaller than specified size as data URLs to avoid requests. { test: [/.bmp$/, /.gif$/, /.jpe?g$/, /.png$/], loader: require.resolve(‘url-loader’), options: { limit: 10000, name: ‘static/media/[name].[hash:8].[ext]’, }, },image-webpack-loader 这是一个可以通过设置质量参数来压缩图片的插件,但个人觉得在实际开发中并不会经常使用,图片一般是UI提供,一般来说,他们是不会同意图片的质量有问题。资源私有化以这种方式加载资源,你可以以更直观的方式将模块和资源组合在一起。无需依赖于含有全部资源的 /assets 目录,而是将资源与代码组合在一起。例如,类似这样的结构会非常有用- |- /assets+ |– /components+ | |– /my-component+ | | |– index.jsx+ | | |– index.css+ | | |– icon.svg+ | | |– img.png当然,这种选择见仁见智Tree-Shaking前端中的tree-shaking就是将一些无关的代码删掉不打包。在Webpack项目中,我们通常会引用很多文件,但实际上我们只引用了其中的某些模块,但却需要引入整个文件进行打包,会导致我们的打包结果变得很大,通过tree-shaking将没有使用的模块摇掉,这样来达到删除无用代码的目的。 Tree-Shaking的原理可以参考这篇文章 归纳起来就是1.ES6的模块引入是静态分析的,故而可以在编译时正确判断到底加载了什么代码。 2.分析程序流,判断哪些变量未被使用、引用,进而删除此代码Tree-Shaking不起作用,代码没有被删? 归纳起来就是因为Babel的转译,使得引用包的代码有了副作用,而副作用会导致Tree-Shaking失效。Webpack 4 默认启用了 Tree Shaking。对副作用进行了消除,以下是我在4.19.1的实验index.jsimport { cube } from ‘./math.js’console.log(cube(5))math.js// 不打包squareexport class square { constructor() { console.log(‘square’) }}export class cube { constructor(x) { return x * x * x }}// babel编译后 同不打包’use strict’;Object.defineProperty(exports, “__esModule”, { value: true});exports.cube = cube;function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError(“Cannot call a class as a function”); } }var square = exports.square = function square() { _classCallCheck(this, square); console.log(‘square’);};function cube(x) { console.log(‘cube’); return x * x * x;}// 不打包export function square(x) { console.log(‘square’) return x.a}export function cube (x) { return x * x * x}// wow 被打包export function square() { console.log(‘square’) return x.a}square({a: 1})export function cube () { return x * x * x}sourcemap简单说,Source map就是一个信息文件,里面储存着位置信息。也就是说,转换后的代码的每一个位置,所对应的转换前的位置。有了它,出错的时候,除错工具将直接显示原始代码,而不是转换后的代码。这无疑给开发者带来了很大方便。 webpack中的devtool配置项可以设置sourcemap,可以参考官方文档然而,devtool的许多选项都讲的不是很清楚,这里推荐该文章,讲的比较详细 要注意,避免在生产中使用 inline- 和 eval-,因为它们可以增加 bundle 大小,并降低整体性能。模块热替换热替换这一块目前大多数都是用的webpack-dev-middleware插件配合服务器使用的,而官方提供的watch模式反而比较少用,当然,webpack-dev-middleware的底层监听watch mode,至于为什么不直接使用watch模式,则是webpack-dev-middleware快速编译,走内存;只依赖webpack的watch mode来监听文件变更,自动打包,每次变更,都将新文件打包到本地,就会很慢。DefinePluginwebpack.DefinePlugin 定义环境变量process.env,这在实际开发中比较常用,参考create-react-app中的代码如下:// Makes some environment variables available to the JS code, for example: // if (process.env.NODE_ENV === ‘development’) { … }. See ./env.js. new webpack.DefinePlugin(env.stringified),不过,要注意不能在config中使用,因为process.env.NODE_ENV === ‘production’ ? ‘[name].[hash].bundle.js’ : ‘[name].bundle.js’NODE_ENV is set in the compiled code, not in the webpack.config.js file. You should not use enviroment variables in your configuration. Pass options via –env.option abc and export a function from the webpack.config.js.大致意思就是NODE_ENV是设置在compiled里面,而不是config文件里。ExtractTextWebpackPluginExtractTextWebpackPlugin,将css抽取成单独文件,可以通过这种方式配合后端对CSS文件进行缓存。SplitChunksPluginwebpack4的代码分割插件。 webpack4中支持了零配置的特性,同时对块打包也做了优化,CommonsChunkPlugin已经被移除了,现在是使用optimization.splitChunks代替。 SplitChunksPlugin的配置有几个需要比较关注一下 chunks: async | initial | allasync: 默认值, 将按需引用的模块打包initial: 分开优化打包异步和非异步模块all: all会把异步和非异步同时进行优化打包。也就是说moduleA在indexA中异步引入,indexB中同步引入,initial下moduleA会出现在两个打包块中,而all只会出现一个。cacheGroups 使用cacheGroups可以自定义配置打包块。 更多详细内容参考该文章动态引入则是利用动态引入的文件打包成另一个包,并懒加载它。其与SplitChunksPlugin的区别:Bundle splitting:实际上就是创建多个更小的文件,并行加载,以获得更好的缓存效果;主要的作用就是使浏览器并行下载,提高下载速度。并且运用浏览器缓存,只有代码被修改,文件名中的哈希值改变了才会去再次加载。Code splitting:只加载用户最需要的部分,其余的代码都遵从懒加载的策略;主要的作用就是加快页面加载速度,不加载不必要加载的东西。参考代码:+ import _ from ’lodash’;++ function component() { var element = document.createElement(‘div’);+ var button = document.createElement(‘button’);+ var br = document.createElement(‘br’);+ button.innerHTML = ‘Click me and look at the console!’; element.innerHTML = _.join([‘Hello’, ‘webpack’], ’ ‘);+ element.appendChild(br);+ element.appendChild(button);++ // Note that because a network request is involved, some indication+ // of loading would need to be shown in a production-level site/app.+ button.onclick = e => import(/* webpackChunkName: “print” */ ‘./print’).then(module => {+ var print = module.default;++ print();+ }); return element; }+ document.body.appendChild(component());注意当调用 ES6 模块的 import() 方法(引入模块)时,必须指向模块的 .default 值,因为它才是 promise 被处理后返回的实际的 module 对象。缓存runtimeChunk因为webpack会把运行时代码放到最后的一个bundle中, 所以即使我们修改了其他文件的代码,最后的一个bundle的hash也会改变,runtimeChunk是把运行时代码单独提取出来的配置。这样就有利于我们和后端配合缓存文件。 配置项single: 所有入口共享一个生成的runtimeChunktrue/mutiple: 每个入口生成一个单独的runtimeChunk模块标识符有时候我们只是添加了个文件print.js, 并在index引入import Print from ‘./print’打包的时候,期望只有runtime和main两个bundle的hash发生改变,但是通常所有bundle都发生了变化,因为每个 module.id 会基于默认的解析顺序(resolve order)进行增量。也就是说,当解析顺序发生变化,ID 也会随之改变。 可以使用两个插件来解决这个问题。第一个插件是 NamedModulesPlugin,将使用模块的路径,而不是数字标识符。虽然此插件有助于在开发过程中输出结果的可读性,然而执行时间会长一些。第二个选择是使用 HashedModuleIdsPlugin。 参考文章ProvidePlugin通过ProvidePlugin处理全局变量 其他更细粒度的处理polyfills的处理首先了解一下polyfills, 虽然在webpack中能够使用es6es7等的API,但并不代表编译器支持这些API,所以通常我们会用polyfills来自定义一个API。 那么在webpack中,一般是使用babel-polyfill VS babel-runtime VS babel-preset-env等来支持这些API,而这三种怎么选择也是一个问题。 在真正进入主题之前,我们先看一个preset-env的配置项,同时也是package.json中的一个配置项browserslist{ “browserslist”: [ “last 1 version”, “> 1%”, “maintained node versions”, “not dead” ]}根据这个配置,preset-env或者postcss等会根据你的参数支持不同的polyfills,具体的参数配置参考该文章 另外推荐一个网站,可以看各种浏览器的使用情况。babel-polyfill 只需要引入一次,但会重写一些原生的已支持的方法,而且体积很大。transform-runtime 是利用 plugin 自动识别并替换代码中的新特性,你不需要再引入,只需要装好 babel-runtime 和 配好 plugin 就可以了。好处是按需替换,检测到你需要哪个,就引入哪个 polyfill,值得注意的是,instance 上新添加的一些方法,babel-plugin-transform-runtime 是没有做处理的,比如 数组的 includes, filter, fill 等babel-preset-env 根据当前的运行环境,自动确定你需要的 plugins 和 polyfills。通过各个 es标准 feature 在不同浏览器以及 node 版本的支持情况,再去维护一个 feature 跟 plugins 之间的映射关系,最终确定需要的 plugins。参考文章后编译日常我们引用的Npm包都是编译好的,这样带来的方便的同时也暴露了一些问题。代码冗余:一般来说,这些 NPM 包也是基于 ES2015+ 开发的,每个包都需要经过 babel 编译发布后才能被主应用使用,而这个编译过程往往会附加很多“编译代码”;每个包都会有一些相同的编译代码,这就造成大量代码的冗余,并且这部分冗余代码是不能通过 Tree Shaking 等技术去除掉的。非必要的依赖:考虑到组件库的场景,通常我们为了方便一股脑引入了所有组件;但实际情况下对于一个应用而言可能只是用到了部分组件,此时如果全部引入,也会造成代码冗余。所以我们自己的公司组件可以采用后编译的形式,即发布的是未经编译的npm包,在项目构建时才编译,我们公司采用的也是这种做法,因为我们的包都在一个目录下,所以不用考虑递归编译的问题。 更多的详细请直接参考该文章设置环境变量这个比较简单,直接看代码或者官方文档即可webpack –env.NODE_ENV=local –env.production –progress其他插件插件总结归类CompressionWebpackPlugin 将文件压缩 文件大小减小很多 需要后端协助配置mini-css-extract-plugin将CSS分离出来wbepack.IgnorePlugin忽略匹配的模块uglifyjs-webpack-plugin代码丑化,webpack4的mode(product)自动配置optimize-css-assets-webpack-plugincss压缩webpack-md5-hash使你的chunk根据内容生成md5,用这个md5取代 webpack chunkhash。总结Webpack本身并不难于理解,难的是各种各样的配置和周围生态带来的复杂,然而也是这种复杂给我们带来了极高的便利性,理解这些有助于在搭建项目更好的优化。后面会继续写出两篇总结,分别是webpack的内部原理流程和webpack的插件开发原理。 ...

February 20, 2019 · 3 min · jiezi

Puppeteer前端自动化测试实践

本篇内容将记录并介绍使用Puppeteer进行自动化网页测试,并依靠约定来避免反复修改测试用例的方案。主要解决页面众多时,修改代码导致的牵连错误无法被发现的运行时问题。文章首发于个人博客起因目前我们在持续开发着一个几十个页面,十万+行代码的项目,随着产品的更迭,总会出现这样的问题。在对某些业务逻辑或者功能进行添加或者修改的时候(尤其是通用逻辑),这些通用的逻辑或者组件往往会牵扯到一些其他地方的问题。由于测试人员受限,我们很难在完成一个模块单元后,对所有功能重新测试一遍。同时,由于环境及数据的区别,(以及在开发过程中对代码完备性的疏忽),代码会在某些特殊数据的解析和和展示上出现问题,在开发和测试中很难去发现。总的来说,我们希望有一个这样的工具,帮我们解决上述几个问题:在进行代码和功能改动后,能够自动访问各个功能的页面,检测问题针对大量的数据内容,进行批量访问,检测对于不同数据的展示是否存在问题测试与代码功能尽量不耦合,避免每次上新功能都需要对测试用例进行修改,维护成本太大定期的测试任务,及时发现数据平台针对新数据的展示完备性其中,最重要的问题,就是将测试代码与功能解耦,避免每次迭代和修改都需要追加新的测试用例。我们如何做到这一点呢?首先我们来梳理下测试平台的功能。功能设定由于我们的平台主要是进行数据展示,所以我们在测试过程中,主要以日常的展示数据为重心即可,针对一些复杂的表单操作先不予处理。针对上述的几个问题,我们针对自动化测试工具的功能如下:依次访问各个页面访问各个页面的具体内容,如时间切换、选项卡切换、分页切换、表格展开行等等针对数据表格中的详情链接,选择前100条进行访问,并进行下钻页的继续测试捕获在页面中的错误请求对错误信息进行捕获,统计和上报根据以上的梳理,我们可以把整个应用分为几个测试单元页面单元,检测各功能页面访问的稳定性详情页单元,根据页面的数据列表,进行批量的详情页跳转,检测不同参数下详情页的稳定性功能单元,用于检测页面和详情页各种展示类型点击切换后是否产生错误通过这样的划分,我们针对各个单元进行具体的测试逻辑书写用例,这样就可以避免再添加新功能和页面时,频繁对测试用例进行修改了。Puppeteer带着上面我们的需求,我们来看下Puppeteer的功能和特性,是否能够满足我们的要求。文档地址Puppeteer是一个Node库,它提供了一个高级 API 来通过 DevTools 协议控制 Chromium 或 Chrome。Puppeteer 默认以 headless 模式运行,但是可以通过修改配置文件运行“有头”模式。我们可以使用Puppeteer完成以下工作:访问页面,进行截图自动进行键盘输入,提交表单模拟点击等用户操作等等等等。。我们来通过一些小案例,来介绍他们的基本功能:访问一个带有ba认证的网站puppeteer可以创建page实例,并使用goto方法进行页面访问,page包含一系列方法,可以对页面进行各种操作。(async () => { const browser = await puppeteer.launch(); const page = await browser.newPage(); // ba认证 await page.authenticate({ username, password }); // 访问页面 await page.goto(‘https://example.com’); // 进行截图 await page.screenshot({path: ’example.png’}); await browser.close();})();访问登陆页面,并进行登录首先,对于SPA(单页面应用),我们都知道,当页面进入后,客户端代码才开始进行渲染工作。我们需要等到页面内容渲染完成后,再进行对应的操作。我们有以下几种方法来使用waitUntilpuppeteer针对页面的访问,切换等,提供了waitUntil参数,来确定满足什么条件才认为页面跳转完成。包括以下事件:load - 页面的load事件触发时domcontentloaded - 页面的DOMContentLoaded事件触发时networkidle0 - 不再有网络连接时触发(至少500毫秒后)networkidle2 - 只有2个网络连接时触发(至少500毫秒后)通过waitUnitl,我们可以当页面请求都完成之后,确定页面已经访问完成。waitForwaitFor方法可以在指定动作完成后才进行resolve// wait for selectorawait page.waitFor(’.foo’);// wait for 1 secondawait page.waitFor(1000);// wait for predicateawait page.waitFor(() => !!document.querySelector(’.foo’));我们可以利用waitForSelector方法,当登录框渲染成功后,才进行登录操作// 等待密码输入框渲染await page.waitFor(’#password’);// 输入用户名await page.type(‘input#username’, “username”);// 输入密码await page.type(‘input#password’, “testpass”);// 点击登录按钮await Promise.all([ page.waitForNavigation(), // 等跳转完成后resolve page.click(‘button.login-button’), // 点击该链接将间接导致导航(跳转)]);await page.waitFor(2000)// 获取cookiesconst cookies = await page.cookies()针对列表内容里的链接进行批量访问主要利用到page实例的选择器功能const table = await page.$(’.table’)const links = await table.$$eval(‘a.link-detail’, links => links.map(link => link.href));// 循环访问links…进行错误和访问监听puppeteer可以监听在页面访问过程中的报错,请求等等,这样我们就可以捕获到页面的访问错误并进行上报啦,这也是我们进行测试需要的基本功能~// 当发生页面js代码没有捕获的异常时触发。page.on(‘pagerror’, () => {})// 当页面崩溃时触发。page.on(’error’, () => {})// 当页面发送一个请求时触发page.on(‘request’)// 当页面的某个请求接收到对应的 response 时触发。page.on(‘response’)通过以上的几个小案例,我们发现Puppeteer的功能非常强大,完全能够满足我们以上的对页面进行自动访问的需求。接下来,我们针对我们的测试单元进行个单元用例的书写最终功能通过我们上面对测试单元的规划,我们可以规划一下我们的测试路径访问网站 -> 登陆 -> 访问页面1 -> 进行基本单元测试 -> 获取详情页跳转链接 -> 依次访问详情页 -> 进行基本单元测试-> 访问页面2 …所以,我们可以拆分出几个大类,和几个测试单元,来进行各项测试// 包含基本的测试方法,log输出等class Base {}// 详情页单元,进行一些基本的单元测试class PageDetal extends Base {}// 页面单元,进行基本的单元测试,并获取并依次访问详情页class Page extends PageDetal {}// 进行登录等操作,并依次访问页面单元进行测试class Root extends Base {}同时,我们如何在功能页面变化时,跟踪到测试的变化呢,我们可以针对我们测试的功能,为其添加自定义标签test-role,测试时,根据自定义标签进行测试逻辑的编写。例如针对时间切换单元,我们做一下简单的介绍:// 1. 获取测试单元的元素const timeSwitch = await page.$(’[test-role=“time-switch”]’);// 若页面没有timeSwitch, 则不用进行测试if (!timeSwitch) return// 2. time switch的切换按钮const buttons = timeSwitch.$$(’.time-switch-button’)// 3. 对按钮进行循环点击for (let i = 0; i < buttons.length; i++) { const button = buttons[i] // 点击按钮 await button.click() // 重点! 等待对应的内容出现时,才认定页面访问成功 try { await page.waitFor(’[test-role=“time-switch-content”]’) } catch (error) { reportError (error) } // 截图 await page.screenshot()}上面只是进行了一个简单的访问内容测试,我们可以根据我们的用例单元书写各自的测试逻辑,在我们日常开发时,只需要对需要测试的内容,加上对应的test-role即可。总结根据以上的功能划分,我们很好的将一整个应用拆分成各个测试单元进行单元测试。需要注意的是,我们目前仅仅是对页面的可访问性进行测试,仅仅验证当用户进行各种操作,访问各个页面单元时页面是否会出错。并没有对页面的具体展示效果进行测试,这样会和页面的功能内容耦合起来,就需要单独的测试用例的编写了。 ...

February 20, 2019 · 1 min · jiezi

【转载】前端工程——基础篇

特别声明本文转载自《前端工程——基础篇》喂喂喂,那个切图的,把页面写好就发给研发工程师套模板吧。你好,切图仔。不知道你的团队如何定义前端开发,据我所知,时至今日仍然有很多团队会把前端开发归类为产品或者设计岗位,虽然身份之争多少有些无谓,但我对这种偏见还是心存芥蒂,酝酿了许久,决定写一个系列的文章,试着从工程的角度系统的介绍一下我对前端,尤其是Web前端的理解。只要我们还把自己的工作看作为一项软件开发活动,那么我相信读过下面的内容你也一定会有所共鸣。前端,是一种GUI软件现如今前端可谓包罗万象,产品形态五花八门,涉猎极广,什么高大上的基础库/框架,拽炫酷的宣传页面,还有屌炸天的小游戏……不过这些一两个文件的小项目并非是前端技术的主要应用场景,更具商业价值的则是复杂的Web应用,它们功能完善,界面繁多,为用户提供了完整的产品体验,可能是新闻聚合网站,可能是在线购物平台,可能是社交网络,可能是金融信贷应用,可能是音乐互动社区,也可能是视频上传与分享平台……从本质上讲,所有Web应用都是一种运行在网页浏览器中的软件,这些软件的图形用户界面(Graphical User Interface,简称GUI)即为前端。如此复杂的Web应用,动辄几十上百人共同开发维护,其前端界面通常也颇具规模,工程量不亚于一般的传统GUI软件:尽管Web应用的复杂程度与日俱增,用户对其前端界面也提出了更高的要求,但时至今日仍然没有多少前端开发者会从软件工程的角度去思考前端开发,来助力团队的开发效率,更有甚者还对前端保留着”如玩具般简单“的刻板印象,日复一日,刀耕火种。历史悠久的前端开发,始终像是放养的野孩子,原始如斯,不免让人慨叹!前端工程的三个阶段现在的前端开发倒也并非一无所有,回顾一下曾经经历过或听闻过的项目,为了提升其前端开发效率和运行性能,前端团队的工程建设大致会经历三个阶段:第一阶段:库/框架选型前端工程建设的第一项任务就是根据项目特征进行技术选型。基本上现在没有人完全从0开始做网站,哪怕是政府项目用个jquery都很正常吧,React/Angularjs等框架横空出世,解放了不少生产力,合理的技术选型可以为项目节省许多工程量这点毋庸置疑。第二阶段:简单构建优化选型之后基本上就可以开始敲码了,不过光解决开发效率还不够,必须要兼顾运行性能。前端工程进行到第二阶段会选型一种构建工具,对代码进行压缩,校验,之后再以页面为单位进行简单的资源合并。前端开发工程化程度之低,常常出乎我的意料,我之前在百度工作时是没有多少概念的,直到离开大公司的温室,去到业界与更多的团队交流才发现,能做到这个阶段在业界来说已然超出平均水平,属于“具备较高工程化程度”的团队了,查看网上形形色色的网页源代码,能做到最基本的JS/CSS压缩的Web应用都已跨入标准互联网公司行列,不难理解为什么很多前端团队对于前端工程构建的认知还仅停留在“压缩、校验、合并”这种程度。第三阶段:JS/CSS模块化开发分而治之是软件工程中的重要思想,是复杂系统开发和维护的基石,这点放在前端开发中同样适用。在解决了基本开发效率运行效率问题之后,前端团队开始思考维护效率,模块化是目前前端最流行的分治手段。很多人觉得模块化开发的工程意义是复用,我不太认可这种看法,在我看来,模块化开发的最大价值应该是分治,是分治,分治!(重说三)。不管你将来是否要复用某段代码,你都有充分的理由将其分治为一个模块。JS模块化方案很多,AMD/CommonJS/UMD/ES6 Module等,对应的框架和工具也一大堆,说起来很烦,大家自行百度吧;CSS模块化开发基本都是在less、sass、stylus等预处理器的import/mixin特性支持下实现的。虽然这些技术由来已久,在如今这个“言必及React”的时代略显落伍,但想想业界的绝大多数团队的工程化落后程度,放眼望去,毫不夸张的说,能达到第三阶段的前端团队已属于高端行列,基本具备了开发维护一般规模Web应用的能力。然而,做到这些就够了么?Naive!第四阶段前端是一种技术问题较少、工程问题较多的软件开发领域。当我们要开发一款完整的Web应用时,前端将面临更多的工程问题,比如:大体量:多功能、多页面、多状态、多系统;大规模:多人甚至多团队合作开发;高性能:CDN部署、缓存控制、文件指纹、缓存复用、请求合并、按需加载、同步/异步加载、移动端首屏CSS内嵌、HTTP 2.0服务端资源推送。扩展阅读:大公司里怎样开发和部署前端代码?这些无疑是一系列严肃的系统工程问题。前面讲的三个阶段虽然相比曾经“茹毛饮血”的时代进步不少,但用于支撑第四阶段的多人合作开发以及精细的性能优化似乎还欠缺点什么。到底,缺什么呢?没有银弹读过《人月神话》的人应该都听说过,软件工程 没有银弹。没错,前端开发同样没有银弹,可是现在是连™铅弹都没有的年月!(刚有了BB弹,摔)前端历来以“简单”著称,在前端开发者群体中,小而美的价值观占据着主要的话语权,甚至成为了某种信仰,想与其他人交流一下工程方面的心得,得到的回应往往都是两个字:太重。重你妹!你的脑容量只有4K吗?工程方案其实也可以小而美!只不过它的小而美不是指代码量,而是指“规则”。找到问题的根源,用最少最简单明了的规则制定出最容易遵守最容易理解的开发规范或工具,以提升开发效率和工程质量,这同样是小而美的典范!2011年我有幸参与到 FIS 项目中,与百度众多大中型项目的前端研发团队共同合作,不断探索实践前端开发的工程化解决方案,13年离开百度去往UC,面对完全不同的产品形态,不同的业务场景,不同的适配终端,甚至不同的网络环境,过往的方法论仍然能够快速落地,为多个团队的不同业务场景量身定制出合理的前端解决方案。这些经历让我明悟了一个道理:进入第四阶段,我们只需做好两件事就能大幅提升前端开发效率,并且兼顾运行性能,那就是——组件化开发与资源管理。第一件事:组件化开发分治的确是非常重要的工程优化手段。在我看来,前端作为一种GUI软件,光有JS/CSS的模块化还不够,对于UI组件的分治也有着同样迫切的需求:如上图,这是我所信仰的前端组件化开发理念,简单解读一下:页面上的每个 独立的 可视/可交互区域视为一个组件;每个组件对应一个工程目录,组件所需的各种资源都在这个目录下就近维护;由于组件具有独立性,因此组件与组件之间可以 自由组合;页面只不过是组件的容器,负责组合组件形成功能完整的界面;当不需要某个组件,或者想要替换组件时,可以整个目录删除/替换。其中第二项描述的就近维护原则,是我觉得最具工程价值的地方,它为前端开发提供了很好的分治策略,每个开发者都将清楚的知道,自己所开发维护的功能单元,其代码必然存在于对应的组件目录中,在那个目录下能找到有关这个功能单元的所有内部逻辑,样式也好,JS也好,页面结构也好,都在那里。组件化开发具有较高的通用性,无论是前端渲染的单页面应用,还是后端模板渲染的多页面应用,组件化开发的概念都能适用。组件HTML部分根据业务选型的不同,可以是静态的HTML文件,可以是前端模板,也可以是后端模板:不同的技术选型决定了不同的组件封装和调用策略。基于这样的工程理念,我们很容易将系统以独立的组件为单元进行分工划分:由于系统功能被分治到独立的模块或组件中,粒度比较精细,组织形式松散,开发者之间不会产生开发时序的依赖,大幅提升并行的开发效率,理论上允许随时加入新成员认领组件开发或维护工作,也更容易支持多个团队共同维护一个大型站点的开发。结合前面提到的模块化开发,整个前端项目可以划分为这么几种开发概念:名称 说明 举例 JS模块 独立的算法和数据单元 浏览器环境检测(detect),网络请求(ajax),应用配置(config),DOM操作(dom),工具函数(utils),以及组件里的JS单元 CSS模块 独立的功能性样式 浏览器环境检测(detect),网络请求(ajax),应用配置(config),DOM操作(dom),工具函数(utils),以及组件里的JS单元 UI组件 独立的可视/可交互功能单元 页头(header),页尾(footer),导航栏(nav),搜索框(search) 页面 前端这种GUI软件的界面状态,是UI组件的容器 首页(index),列表页(list),用户管理(user) 应用 整个项目或整个站点被称之为应用,由多个页面组成 以上5种开发概念以相对较少的规则组成了前端开发的基本工程结构,基于这些理念,我眼中的前端开发就成了这个样子:示意图 描述 整个Web应用由页面组成 页面由组件组成 一个组件一个目录,资源就近维护 组件可组合,组件的JS可依赖其他JS模块,CSS可依赖其他CSS单元综合上面的描述,对于一般中小规模的项目,大致可以规划出这样的源码目录结构:如果项目规模较大,涉及多个团队协作,还可以将具有相关业务功能的页面组织在一起,形成一个子系统,进一步将整个站点拆分出多个子系统来分配给不同团队维护,针对这种情况后面我会单开文章详细介绍。以上架构设计历经许多不同公司不同业务场景的前端团队验证,收获了不错的口碑,是行之有效的前端工程分治方案。吐槽:我本人非常反对某些前端团队将前端开发划分为“JS开发”和“页面重构”两种岗位,更倾向于组件粒度的开发理念,对GUI软件开发的分工规划应该以功能为单位,而不是开发语言;对开发者的技术要求也应该是掌握完整的端内技术。第二件事:“智能”静态资源管理上面提到的模块化/组件化开发,仅仅描述了一种开发理念,也可以认为是一种开发规范,倘若你认可这规范,对它的分治策略产生了共鸣,那我们就可以继续聊聊它的具体实现了。很明显,模块化/组件化开发之后,我们最终要解决的,就是模块/组件加载的技术问题。然而前端与客户端GUI软件有一个很大的不同:前端是一种远程部署,运行时增量下载的GUI软件前端应用没有安装过程,其所需程序资源都部署在远程服务器,用户使用浏览器访问不同的页面来加载不同的资源,随着页面访问的增加,渐进式的将整个程序下载到本地运行,“增量下载”是前端在工程上有别于客户端GUI软件的根本原因。上图展示了一款界面繁多功能丰富的应用,如果采用Web实现,相信也是不小的体量,如果用户第一次访问页面就强制其加载全站静态资源再展示,相信会有很多用户因为失去耐心而流失。根据“增量”的原则,我们应该精心规划每个页面的资源加载策略,使得用户无论访问哪个页面都能按需加载页面所需资源,没访问过的无需加载,访问过的可以缓存复用,最终带来流畅的应用体验。这正是Web应用“免安装”的魅力所在。由“增量”原则引申出的前端优化技巧几乎成为了性能优化的核心,有加载相关的按需加载、延迟加载、预加载、请求合并等策略;有缓存相关的浏览器缓存利用,缓存更新、缓存共享、非覆盖式发布等方案;还有复杂的BigRender、BigPipe、Quickling、PageCache等技术。这些优化方案无不围绕着如何将增量原则做到极致而展开。所以我觉得:第四阶段前端开发最迫切需要做好的就是在基础架构中贯彻增量原则。相信这种贯彻不会随着时间的推移而改变,在可预见的未来,无论在HTTP1.x还是HTTP2.0时代,无论在ES5亦或者ES6/7时代,无论是AMD/CommonJS/UMD亦或者ES6 module时代,无论端内技术如何变迁,我们都有足够充分的理由要做好前端程序资源的增量加载。正如前面说到的,第三阶段前端工程缺少点什么呢?我觉得是在其基础架构中缺少这样一种“智能”的资源加载方案。没有这样的方案,很难将前端应用的规模发展到第四阶段,很难实现落地前面介绍的那种组件化开发方案,也很难让多方合作高效率的完成一项大型应用的开发,并保证其最终运行性能良好。在第四阶段,我们需要强大的工程化手段来管理”玩具般简单“的前端开发。在我的印象中,Facebook是这方面探索的伟大先驱之一,早在2010年的Velocity China大会上,来自Facebook的David Wei博士就为业界展示了他们令人惊艳的静态网页资源管理和优化技术。David Wei博士在当年的交流会上提到过一些关于Facebook的一些产品数据:Facebook整站有10000+个静态资源;每个静态资源都有可能被翻译成超过100种语言版本;每种资源又会针对浏览器生成3种不同的版本;要针对不同带宽的用户做5种不同的打包方法;有3、4个不同的用户组,用于小批次体验新的产品功能;还要考虑不同的送达方法,可以直接送达,或者通过iframe的方式提升资源并行加载的速度;静态资源的压缩和非压缩状态可切换,用于调试和定位线上问题这是一个状态爆炸的问题,将所有状态乘起来,整个网站的资源组合方式会达到几百万种之多(去重之后统计大概有300万种组合方式)。支撑这么大规模前端项目运行的底层架构正是魏博士在那次演讲中分享的Static Resource Management System(静态资源管理系统),用以解决Facebook项目中有关前端工程的3D问题(Development,Deployment,Debugging)。那段时间 FIS 项目正好遇到瓶颈,当时的FIS还是一个用php写的task-based构建工具,那时候对于前端工程的认知度很低,觉得前端构建不就是几个压缩优化校验打包任务的组合吗,写好流程调度,就针对不同需求写插件呗,看似非常简单。但当我们支撑越来越多的业务团队,接触到各种不同的业务场景时,我们深刻的感受到task-based工具的粗糙,团队每天疲于根据各种业务场景编写各种打包插件,构建逻辑异常复杂,隐隐看到不可控的迹象。我们很快意识到把基础架构放到构建工具中实现是一件很愚蠢的事,试图依靠构建工具实现各种优化策略使得构建变成了一个巨大的黑盒,一旦发生问题,定位起来非常困难,而且每种业务场景都有不同的优化需求,构建工具只能通过静态分析来优化加载,具有很大的局限性,单页面/多页面/PC端/移动端/前端渲染/后端渲染/多语言/多皮肤/高级优化等等资源加载问题,总不能给每个都写一套工具吧,更何况这些问题彼此之间还可以有多种组合应用,工具根本写不过来。Facebook的做法无疑为我们亮起了一盏明灯,不过可惜它并不开源(不是技术封锁,而是这个系统依赖FB体系中的其他方面,通用性不强,开源意义不大),我们只能尝试挖掘相关信息,网上对它的完整介绍还是非常非常少,分析facebook的前端代码也没有太多收获,后来无意中发现了facebook使用的项目管理工具phabricator中的一个静态管理方案Celerity,以及相关的说明,看它的描述很像是Facebook静态资源管理系统的一个mini版!简单看过整个系统之后发现原理并不复杂(小而美的典范),它是通过一个小工具扫描所有静态资源,生成一张资源表,然后有一个PHP实现的资源管理框架(Celerity)提供了资源加载接口,替代了传统的script/link等静态的资源加载标签,最终通过查表来加载资源。虽然没有真正看过FB的那套系统,但眼前的这个小小的框架给了当时的我们足够多的启示:静态资源管理系统 = 资源表 + 资源加载框架多么优雅的实现啊!资源表是一份数据文件(比如JSON),是项目中所有静态资源(主要是JS和CSS)的构建信息记录,通过构建工具扫描项目源码生成,是一种k-v结构的数据,以每个资源的id为key,记录了资源的类别、部署路径、依赖关系、打包合并等内容,比如:{ “a.js”: { “url”: “/static/js/a.5f100fa.js”, “dep”: [ “b.js”, “a.css” ] }, “a.css”: { “url”: “/static/css/a.63cf374.css”, “dep”: [ “button.css” ] }, “b.js”: { “url”: “/static/js/b.97193bf.js” }, “button.css”: { “url”: “/static/css/button.de33108.css” }}而资源加载框架则提供一些资源引用的API,让开发者根据id来引用资源,替代静态的script/link标签来收集、去重、按需加载资源。调用这些接口时,框架通过查表来查找资源的各项信息,并递归查找其依赖的资源的信息,然后我们可以在这个过程中实现各种性能优化算法来“智能”加载资源。根据业务场景的不同,加载框架可以在浏览器中用JS实现,也可以是后端模板引擎中用服务端语言实现,甚至二者的组合,不一而足。有关加载框架的具体实现我曾写过很多文章介绍,可以扩展阅读:前端工程与性能优化前端工程与模块化框架这种设计很快被验证具有足够的灵活性,能够完美支撑不同团队不同技术规范下的性能优化需求,前面提到的按需加载、延迟加载、预加载、请求合并、文件指纹、CDN部署、Bigpipe、Quickling、BigRender、首屏CSS内嵌、HTTP 2.0服务端推送等等性能优化手段都可以很容易的在这种架构上实现,甚至可以根据性能日志自动进行优化(Facebook已实现)。因为有了资源表,我们可以很方便的控制资源加载,通过各种手段在运行时计算页面的资源使用情况,从而获得最佳加载性能。无论是前端渲染的单页面应用,还是后端渲染的多页面应用,这种方法都同样适用。此外,它还很巧妙的约束了构建工具的职责——只生成资源表。资源表是非常通用的数据结构,无论什么业务场景,其业务代码最终都可以被扫描为相同结构的表数据,并标记资源间的依赖关系,有了表之后我们只需根据不同的业务场景定制不同的资源加载框架就行了,从此彻底告别一个团队维护一套工具的时代!!!恩,如你所见,虽然彻底告别了一个团队一套工具的时代,但似乎又进入了一个团队一套框架的时代。其实还是有差别的,因为框架具有很大的灵活性,而且不那么黑盒,采用框架实现资源管理相比构建更容易调试、定位和升级变更。深耕静态资源加载框架可以带来许多收益,而且有足够的灵活性和健壮性面向未来的技术变革,这个我们留作后话。总结回顾一下前面提到过的前端工程三个阶段:第一阶段:库/框架选型第二阶段:简单构建优化第三阶段:JS/CSS模块化开发现在补充上第四阶段:第四阶段:组件化开发与资源管理由于先天缺陷,前端相比其他软件开发,在基础架构上更加迫切的需要组件化开发和资源管理,而解决资源管理的方法其实一点也不复杂:一个通用的资源表生成工具 + 基于表的资源加载框架近几年来各种你听到过的各种资源加载优化策略大部分都可以在这样一套基础上实现,而这种优化对于业务来说是完全透明的,不需要重构的性能优化——这不正是我们一直所期盼的吗?正如魏小亮博士所说:我们可以把优秀的人集中起来去优化加载。如何选型技术、如何定制规范、如何分治系统、如何优化性能、如何加载资源,当你从切图开始转变为思考这些问题的时候,我想说:你好,工程师!前端工程其实是一个很大的话题,开发仅是其中的一部分。 ...

February 15, 2019 · 1 min · jiezi

webpack4系列教程(十):总结

在前端开发日益复杂的今天,我们需要一个工具来帮助我们管理项目资源,打包、编译、预处理、后处理等等。webpack的出现无疑是前端开发者的福音,我的博文只是本人的学习经验和积累,如果能对你有所帮助那是再好不过了。我的JS真的很一般,所以如果有误人子弟的地方希望你能指出来,大家一起学习,共同进步。话说互联网寒冬来了,我却在这个时候裸辞了(对公司已经彻底绝望了),明年准备去杭州看看,也不知道行情怎么样,祝福自己。传送门webpack4系列教程(一):初识webpackwebpack4系列教程(二):创建项目,打包第一个JS文件webpack4系列教程(三):自动生成项目中的HTML文件webpack4系列教程(四):处理项目中的资源文件(一)webpack4系列教程(五):处理项目中的资源文件(二)webpack4系列教程(六):使用SplitChunksPlugin分割代码webpack4系列教程(七):使用 babel-loaderwebpack4系列教程(八):使用Eslint审查代码webpack4系列教程(九):开发环境和生产环境

January 27, 2019 · 1 min · jiezi

Javascript五十问——从源头细说Webpack与Gulp

前言:Webpack 与 gulp是目前圈子内比较活跃的前端构建工具。网上有很多二者比较的文章,面试中也会经常遇到gulp,Webpack的区别这样的问题。对于初学者来说,对这二者往往容易认识不清,今天,就从事件的源头,说清楚Webpack与gulp。Gulp那是2014年,虽然JQuery风光多年,但是前端却暗流涌动;MVVM刚刚提出不久,Angular快速成长,而React和Vue也刚刚开源不到一年,尚属于冷门小语种。那个时候,前端工作者面临的主要矛盾在于日益增长的业务复杂化的需求同落后低效率的前端部署。开发工作者为了发布一个网站,往往会重复的进行一些与开发无关的工作,手动完成这些工作会带来很大的挫败感。这个时候,自动化构建工具及应运而生,gulp就是在大浪淘沙中的胜利者。Gulp是基于流的前端构建工具,nodejs的stream操作来读取和操作数据;可以实现文件的转换,压缩,合并,监听,自动部署等功能。gulp拥有强大的插件库,基本上满足开发需求,而且开发人员也可以根据自己的需求开发自定义插件。难得是,gulp只有五个api,容易上手。const gulp = require(‘gulp’);const sass = require(“gulp-sass”)gulp.task(“sassStyle”,function() { gulp.src(“style/*.scss”) .pipe(sass()) .pipe(gulp.dest(“style”))})上面就是一个基本的gulpfile配置文件,实现了scss文件到css文件的转换;在终端输入gulp sassStyle就能够进行文件处理了。对于gulp而言,会有一个task,这个task只会做一件事,比如将sass格式的文档转换成css文件;对于一个task而言,会有一个入口文件,即gulp.src,最会有一个目标文件,即gulp.dest;一入一出,可以将gulp理解为 一元函数,输入一个x,根据funcion产出一个y。Gulp简单,快速,自动化的构建方案,收获了很多开发者的喜爱。但是怎样的机遇,让webpack占据了前端工程化的半壁江山呢?Webpack解决方案永远是紧跟需求的脚步的。随着React与Vue份额越来越大,spa开发模式应用在越来越多的领域中,而ES6 Module语法的提出与大规模应用,模块化开发方式越来越受人们的青睐。致使前端文件之间的依赖性越来越高,这时候就需要一个工具能够解析这些依赖,并且将它们有条理的打包起来,优化请求,最好顺便能够解析成浏览器可以识别的语言——这正是webpack所承担的工作;而很多开发者,也是从react或者vue的项目入手webpack的。图片来源于互联网,侵删Webpack 是前端资源模块化 管理和打包工具。它可以将许多松散的模块按照依赖和规则打包成符合生产环境部署的前端资源。还可以将按需加载的模块进行代码分割,等到实际需要的时候再异步加载——来自Webpack官方网站。所以Webpack只完成两件事:按需加载,打包。module.exports = { // 入口文件,是模块构建的起点,同时每一个入口文件对应最后生成的一个 chunk。 entry: { bundle: [ ‘webpack/hot/dev-server’, ‘webpack-dev-server/client?http://localhost:8080’, path.resolve(__dirname, ‘app/app.js’) ] }, // 文件路径指向(可加快打包过程)。 resolve: { alias: { ‘react’: pathToReact } }, // 生成文件,是模块构建的终点,包括输出文件与输出路径。 output: { path: path.resolve(__dirname, ‘build’), filename: ‘[name].js’ }, // 这里配置了处理各模块的 loader ,包括 css 预处理 loader ,es6 编译 loader,图片处理 loader。 module: { loaders: [ { test: /.js$/, loader: ‘babel’, query: { presets: [’es2015’, ‘react’] } } ], noParse: [pathToReact] }, // webpack 各插件对象,在 webpack 的事件流中执行对应的方法。 plugins: [ new webpack.HotModuleReplacementPlugin() ]};上面是比较简单的webpack配置文件 webpack.config.js,如果说gulp是一个一元函数,那么,webpack就是一个多元函数或者是加工厂;webpack从入口文件开始,递归找出所有依赖的代码块,再将代码块按照loader解析成新内容,而在webpack会在各个特定的时期广播对应事件,插件会监听这些事件,在某个事件中进行特定的操作。通俗一点来说,webpack本身来递归找到各个文件之间的依赖关系,在这个过程中,使用loaders对文件进行解析,最后,在各个不同的事件阶段,插件可以对文件进行一个统一的处理。webpack.config文件会包括以下几部分:1.entry:入口,webpack问此文件入手迭代。2.output: 打包后形成的文件出口。3.module: 模块,在webpack中一个模块对应一个文件。webpack会从entry开始,递归找出所有依赖的模块4.loaders:文件解析的各种转换器5.plugin:拓展插件webpack的配置文件和构建方式比较复杂,这里不再赘述,感兴趣的同学可以参考我列出来的参考文献第三篇文章,或者可以关注我的专栏,后期我会出一篇关于webpack的学习笔记。比较所以,我们可以看出来,虽然Webpack与gulp都是前端工程化的管理工具,但是二者的侧重点不同——gulp更加关注的是自动化的构建工具,你把代码写好了,gulp会帮你编译、压缩、解析。而Webpack关注的是在模块化背景下的打包工作;它侧重的还是如何将依赖的文件合理的组织起来,并且实现按需加载。总结总的来说,虽然webpack以打包起家,但是gulp能够实现的功能,Webpack也能做;那么,是不是我们以后都要唯webpack马首是瞻呢?非也,非也!webpack功能强大,但是它的缺点也来自于此;webpack并非一个轻量级的工具,学习曲线也非gulp那般平缓。曾经,gulp为了弥补js打包方面的不足,也有gulp-webpack插件的出现;但是webpack强大如斯,如果仅仅只是解析es6文件,未免有大马拉小车之感。根据我的项目实践经验,如果你要构建一个复杂的项目,项目使用vue或者react,模块化引领,那么请选择Webpack,Webpack天生模块化,更加适合于SPA的应用场景,而gulp在SPA下明显后力不足。如果你只是开发一个工具,请选择gulp,至于js打包这种工作,有更加专一的rollup。毕竟,如果只是写一个年会抽奖工具活跃气氛,就不需要webpack火种送碳了。总结下来:gulp与Webapck是各有所长,并不存在东风压倒西风,而在前端工程化的大旗下,并非只有Webpack与gulp,我们还能看到rollup与browserify的一席之地。因此,在真正的工作中,还是要结合项目本身特点,切忌人云亦云。参考文献1、JavaScript开发者的工具箱 非常实用2、Gulp官网3、超级详细的Webpack解读—五星推荐4、端构建工具之争——Webpack vs Gulp 谁会被拍死在沙滩上—五星推荐 ...

January 17, 2019 · 1 min · jiezi

webpack4系列教程(八):使用Eslint审查代码

前言:本章内容,我们在项目中加入eslint配置,来审查校验代码,这样能够避免一些比较低级的错误。并且在团队协作的时候,保持同一种风格和规范能提高代码的可读性,进而提高我们的工作效率。安装:eslint-config-standard 是一种较为成熟通用的代码审查规则,这样就不用我们自己去定义规则了,使用起来非常方便,记住还需要安装一些依赖插件:npm install –save-dev eslint eslint-config-standard eslint-plugin-standard eslint-plugin-promise eslint-plugin-import eslint-plugin-node配置:在项目根目录下创建 .eslintrc 文件:{ “extends”: “standard”, “rules”: { “no-new”: “off” }}在vue项目中,.vue文件中的 script标签内的代码,eslint 是无法识别的,这时就需要使用插件: eslint-plugin-htmlnpm i eslint-plugin-html -D然后在 .eslintrc 中配置该插件:{ “extends”: “standard”, “plugins”: [ “html” ], “rules”: { “no-new”: “off” }}这样就能解析 .vue文件中的JS代码了,官方也是如此推荐。使用:配置完成,如何使用呢?在 package.json 文件中添加一条 script:“scripts”: { “build”: “cross-env NODE_ENV=production webpack –config config/webpack.config.js –progress –inline –colors”, “lint”: “eslint –ext .js –ext .vue src/” }- -ext 代表需要解析的文件格式,最后接上文件路径,由于我们的主要代码都在src 目录下,这里就配置 src 文件夹。npm run lint可见控制台给出了很多错误: 在项目前期没有加入eslint的情况下,后期加入必然会审查出许多错误。出现这么多错误之后,如果我们逐条手动去解决会非常耗时,此时可以借助eslint自动修复,方法也很简单。 只需要添加一条命令即可:“scripts”: { “build”: “cross-env NODE_ENV=production webpack –config config/webpack.config.js –progress –inline –colors”, “lint”: “eslint –ext .js –ext .vue src/”, “lint-fix”: “eslint –fix –ext .js –ext .jsx –ext .vue src/"}然后执行npm run lint-fix我们希望在开发过程中能够实时进行eslint代码审查,需要安装两个依赖:npm i eslint-loader babel-eslint -D修改 .eslintrc:{ “extends”: “standard”, “plugins”: [ “html” ], “rules”: { “no-new”: “off” }, “parserOptions”:{ “parser”: “babel-eslint” }}由于我们的项目使用了webpack并且代码都是经过Babel编译的,但是Babel处理过的代码有些语法可能对于eslint支持性不好,所以需要指定一个 parser。下一步,在webpack.config.js中添加loader:{ test: /.(vue|js)$/, loader: ’eslint-loader’, exclude: /node_modules/, enforce: ‘pre’ }enforce: ‘pre’ 表示预处理,因为我们只是希望eslint来审查我们的代码,并不是去改变它,在真正的loader(比如:vue-loader)发挥作用前用eslint去检查代码。记得在你的IDE中安装并开启eslint插件功能,这样就会有错误提示了。比如: 图中的错误是未使用的变量。# editorconfig: editorconfig是用来规范我们的IDE配置的,在根目录创建 .editorconfig文件:root = true[*]charset = utf-8indent_style = spaceindent_size = 2end_of_line = lfinsert_final_newline = truetrim_trailing_whitespace = true这样就能在各种IDE使用相同的配置了。同样需要在IDE中安装editorconfig插件以上就是eslint的配置方法了。本人才疏学浅,如有不当之处,欢迎批评指正 ...

January 15, 2019 · 1 min · jiezi

webpack4系列教程(七):使用 babel-loader

什么是Babel如今 ES6 语法在开发中已经非常普及,甚至也有许多开发人员用上了 ES7 或 ES8 语法。然而,浏览器对这些高级语法的支持性并不是非常好。因此为了让我们的新语法能在浏览器中都能顺利运行,Babel 应运而生。Babel是一个JavaScript编译器,能够让我们放心的使用新一代JS语法。比如我们的箭头函数:() => console.log(‘hello babel’)经过Babel编译之后:(function(){ return console.log(‘hello babel’);});会编译成浏览器可识别的ES5语法。2. 在webpack中使用babel-loader安装:npm install -D babel-loader @babel/core @babel/preset-env webpack修改 webpack.config.js,加入新的loader:{ test: /.js$/, loader: ‘babel-loader’, exclude: /node_modules/}遇到JS文件就先用babel-loader处理,exclude表示排除 node_modules 文件夹中的文件。loader的配置就OK了,可是这样还不能发挥Babel的作用。在项目根目录下创建一个 .babelrc 文件,添加代码:{ “presets”: [ “@babel/preset-env” ]}我们还希望能够在项目对一些组件进行懒加载,所以还需要一个Babel插件:npm i babel-plugin-syntax-dynamic-import -D在 .babelrc 文件中加入plugins配置:{ “presets”: [ “@babel/preset-env” ], “plugins”: [ “syntax-dynamic-import” ]}在src 目录下创建 helper.js:console.log(’this is helper’)再来修改我们的 main.js :import ‘babel-polyfill’import Modal from ‘./components/modal/modal’import ‘./assets/style/common.less’import _ from ’lodash’const App = function () { let div = document.createElement(‘div’) div.setAttribute(‘id’, ‘app’) document.body.appendChild(div) let dom = document.getElementById(‘app’) let modal = new Modal() dom.innerHTML = modal.template({ title: ‘标题’, content: ‘内容’, footer: ‘底部’ }) let button = document.createElement(‘button’) button.innerText = ‘click me’ button.onclick = () => { const help = () => import(’./helper’) help() } document.body.appendChild(button)}const app = new App()console.log(_.camelCase(‘Foo Bar’))当button点击时,加载 helper 然后调用。打包之后可见:多了一个 3.bundle.js,在浏览器打开 dist/index.html ,打开 network查看,3.bundle.js并未加载:当点击button之后,发现浏览器请求了3.bundle.js,控制台也打印出了数据。由于 Babel 只转换语法(如箭头函数), 你可以使用 babel-polyfill 支持新的全局变量,例如 Promise 、新的原生方法如 String.padStart (left-pad) 等。安装:npm install –save-dev babel-polyfill在入口文件引入就可以了:import ‘babel-polyfill’本人才疏学浅,不当之处欢迎批评指正。

January 15, 2019 · 1 min · jiezi

webpack4系列教程(六):使用SplitChunksPlugin分割代码

1. SplitChunksPlugin的概念起初,chunks(代码块)和导入他们中的模块通过webpack内部的父子关系图连接.在webpack3中,通过CommonsChunkPlugin来避免他们之间的依赖重复。而在webpack4中CommonsChunkPlugin被移除,取而代之的是 optimization.splitChunks 和 optimization.runtimeChunk 配置项,下面展示它们将如何工作。在默认情况下,SplitChunksPlugin 仅仅影响按需加载的代码块,因为更改初始块会影响HTML文件应包含的脚本标记以运行项目。webpack将根据以下条件自动拆分代码块:会被共享的代码块或者 node_mudules 文件夹中的代码块体积大于30KB的代码块(在gz压缩前)按需加载代码块时的并行请求数量不超过5个加载初始页面时的并行请求数量不超过3个举例1:// index.js// 动态加载 a.jsimport(’./a’)// a.jsimport ‘vue’// …打包之后的结果会创建一个包含 vue 的独立代码块,当包含 a.js 的原始代码块被调用时,这个独立代码块会并行请求进来。 原因:vue 来自 node_modules 文件夹vue 体积超过30KB导入调用时的并行请求数为2不影响页面初始加载我们这样做的原因是因为,vue代码并不像你的业务代码那样经常变动,把它单独提取出来就可以和你的业务代码分开缓存,极大的提高效率。举例2:// entry.jsimport("./a");import("./b");// a.jsimport “./helpers”; // helpers is 40kb in size// …// b.jsimport “./helpers”;import “./more-helpers”; // more-helpers is also 40kb in size// …结果:将创建一个单独的块,其中包含./helpers它的所有依赖项。在导入调用时,此块与原始块并行加载。原因:条件1:helpers 是共享块条件2:helpers大于30kb条件3:导入调用的并行请求数为2条件4:不影响初始页面加载时的请求2. SplitChunksPlugin的默认配置以下是SplitChunksPlugin的默认配置:splitChunks: { chunks: “async”, minSize: 30000, // 模块的最小体积 minChunks: 1, // 模块的最小被引用次数 maxAsyncRequests: 5, // 按需加载的最大并行请求数 maxInitialRequests: 3, // 一个入口最大并行请求数 automaticNameDelimiter: ‘~’, // 文件名的连接符 name: true, cacheGroups: { // 缓存组 vendors: { test: /[\/]node_modules[\/]/, priority: -10 }, default: { minChunks: 2, priority: -20, reuseExistingChunk: true } }}缓存组:缓存组因该是SplitChunksPlugin中最有趣的功能了。在默认设置中,会将 node_mudules 文件夹中的模块打包进一个叫 vendors的bundle中,所有引用超过两次的模块分配到 default bundle 中。更可以通过 priority 来设置优先级。chunks:chunks属性用来选择分割哪些代码块,可选值有:‘all’(所有代码块),‘async’(按需加载的代码块),‘initial’(初始化代码块)。3. 在项目中添加SplitChunksPlugin为了方便演示,我们先安装两个类库: lodash 和 axios,npm i lodash axios -S修改 main.js,引入 lodash 和axios 并调用相应方法:import Modal from ‘./components/modal/modal’import ‘./assets/style/common.less’import _ from ’lodash’import axios from ‘axios’const App = function () { let div = document.createElement(‘div’) div.setAttribute(‘id’, ‘app’) document.body.appendChild(div) let dom = document.getElementById(‘app’) let modal = new Modal() dom.innerHTML = modal.template({ title: ‘标题’, content: ‘内容’, footer: ‘底部’ })}const app = new App()console.log(_.camelCase(‘Foo Bar’))axios.get(‘aaa’)使用SplitChunksPlugin不需要安装任何依赖,只需在 webpack.config.js 中的 config对象添加 optimization 属性:optimization: { splitChunks: { chunks: ‘initial’, automaticNameDelimiter: ‘.’, cacheGroups: { vendors: { test: /[\/]node_modules[\/]/, priority: 1 } } }, runtimeChunk: { name: entrypoint => manifest.${entrypoint.name} } }配置 runtimeChunk 会给每个入口添加一个只包含runtime的额外的代码块,name 的值也可以是字符串,不过这样就会给每个入口添加相同的 runtime,配置为函数时,返回当前的entry对象,即可分入口设置不同的runtime。我们再安装一个 webpack-bundle-analyzer,这个插件会清晰的展示出打包后的各个bundle所依赖的模块:npm i webpack-bundle-analyzer -D引入:const BundleAnalyzerPlugin = require(‘webpack-bundle-analyzer’).BundleAnalyzerPlugin使用,在plugins数组中添加即可:new BundleAnalyzerPlugin()打包之后: 各个模块依赖清晰可见,打开 dist/index.html可见我们的代码顺利运行: 以上就是SplitChunksPlugin的基本用法,更多高级的配置大家可以继续钻研(比如多入口应用)。本人才疏学浅,不当之处欢迎批评指正。 ...

January 15, 2019 · 1 min · jiezi

webpack4系列教程(五):处理项目中的资源文件(二)

在项目中使用 less 在 src/assets/ 下新建 common.less :body{ background: #fafafa; padding: 20px;}在 main.js 中引入 common.less :import ‘./assets/style/common.less’安装 less-loader:npm i less-loader -D添加 rules: { test: /.less$/, use: [ ‘style-loader’, ‘css-loader’, ’less-loader’ ] }打包之后,在浏览器打开 dist/index.html,less文件中的样式已经通过 style 标签载入了: 2. 使用MiniCssExtractPlugin我们之前的样式代码都是通过 style 标签载入的,那么如何通过 link 引入CSS文件的方式实现呢?这就需要使用一个插件,在webpack3中通常使用ExtractTextWebpackPlugin,但是在webpack4中已经不再支持ExtractTextWebpackPlugin的正式版,而测试版本又不够稳定,因此我们使用MiniCssExtractPlugin替代。首先安装:npm install –save-dev mini-css-extract-plugin在webpack.config.js 中引入并添加 plugins :const MiniCssExtractPlugin = require(‘mini-css-extract-plugin’)new MiniCssExtractPlugin({ filename: “[name].css” }), 修改 CSS 和 less 的 rules:{ test: /.css$/, use: [ // ‘style-loader’, { loader: MiniCssExtractPlugin.loader }, ‘css-loader’ ] }, { test: /.less$/, use: [ // ‘style-loader’, { loader: MiniCssExtractPlugin.loader }, ‘css-loader’, ’less-loader’ ] }npm run build 之后,可见head中引入了一个 main.css 文件: 也正是我们在 common.less 和 modal.css 中的代码 3. postcss-loaderpostcss-loader 可以帮助我们处理CSS,如自动添加浏览器前缀。npm i -D postcss-loader autoprefixer在根目录下创建 postcss.config.js:const autoprefixer = require(‘autoprefixer’)module.exports = { plugins: [ autoprefixer({ browsers: [’last 5 version’] }) ]}修改 css 和 less 的 rules:{ test: /.css$/, use: [ // ‘style-loader’, { loader: MiniCssExtractPlugin.loader }, { loader: ‘css-loader’, options: { importLoaders: 1 } }, ‘postcss-loader’ ] }, { test: /.less$/, use: [ // ‘style-loader’, { loader: MiniCssExtractPlugin.loader }, ‘css-loader’, ‘postcss-loader’, ’less-loader’ ] }在 modal.css中加入:.flex{ display: flex;}打包之后打开 main.css,可见浏览器前缀已经加上了: 本人才疏学浅,不当之处欢迎批评指正

January 14, 2019 · 1 min · jiezi

[实践系列]Babel原理

前言[实践系列] 主要是让我们通过实践去加深对一些原理的理解。实践系列-前端路由 有兴趣的同学可以关注 实践系列 。 求star求followBabel是什么?我们为什么要了解它?1. 什么是babel ?Babel 是一个 JavaScript 编译器。他把最新版的javascript编译成当下可以执行的版本,简言之,利用babel就可以让我们在当前的项目中随意的使用这些新最新的es6,甚至es7的语法。为了能用可爱的ES678910写代码,我们必须了解它!2. 可靠的工具来源于可怕的付出August 27, 2018 by Henry Zhu历经 2 年,4k 多次提交,50 多个预发布版本以及大量社区援助,我们很高兴地宣布发布 Babel 7。自 Babel 6 发布以来,已经过了将近三年的时间!发布期间有许多要进行的迁移工作,因此请在发布第一周与我们联系。Babel 7 是更新巨大的版本:我们使它编译更快,并创建了升级工具,支持 JS 配置,支持配置 “overrides”,更多 size/minification 的选项,支持 JSX 片段,支持 TypeScript,支持新提案等等!Babel开发团队这么辛苦的为开源做贡献,为我们开发者提供更完美的工具,我们为什么不去了解它呢?(OS:求求你别更啦.老子学不动啦)3. Babel担任的角色August 27, 2018 by Henry Zhu我想再次介绍下过去几年中 Babel 在 JavaScript 生态系统中所担任的角色,以此展开本文的叙述。起初,JavaScript 与服务器语言不同,它没有办法保证对每个用户都有相同的支持,因为用户可能使用支持程度不同的浏览器(尤其是旧版本的 Internet Explorer)。如果开发人员想要使用新语法(例如 class A {}),旧浏览器上的用户只会因为 SyntaxError 的错误而出现屏幕空白的情况。Babel 为开发人员提供了一种使用最新 JavaScript 语法的方式,同时使得他们不必担心如何进行向后兼容,如(class A {} 转译成 var A = function A() {})。由于它能转译 JavaScript 代码,它还可用于实现新的功能:因此它已成为帮助 TC39(制订 JavaScript 语法的委员会)获得有关 JavaScript 提案意见反馈的桥梁,并让社区对语言的未来发展发表自己的见解。Babel 如今已成为 JavaScript 开发的基础。GitHub 目前有超过 130 万个仓库依赖 Babel,每月 npm 下载量达 1700 万次,还拥有数百个用户,其中包括许多主要框架(React,Vue,Ember,Polymer)以及著名公司(Facebook,Netflix,Airbnb)等。它已成为 JavaScript 开发的基础,许多人甚至不知道它正在被使用。即使你自己没有使用它,但你的依赖很可能正在使用 Babel。即使你自己没有使用它,但你的依赖很可能正在使用 Babel。怕不怕 ? 了解不了解 ?Babel的运行原理1.解析解析步骤接收代码并输出 AST。 这个步骤分为两个阶段:词法分析(Lexical Analysis) 和 语法分析(Syntactic Analysis)。1.词法分析词法分析阶段把字符串形式的代码转换为 令牌(tokens) 流。 你可以把令牌看作是一个扁平的语法片段数组: n * n;[ { type: { … }, value: “n”, start: 0, end: 1, loc: { … } }, { type: { … }, value: “”, start: 2, end: 3, loc: { … } }, { type: { … }, value: “n”, start: 4, end: 5, loc: { … } }, …]每一个 type 有一组属性来描述该令牌:{ type: { label: ’name’, keyword: undefined, beforeExpr: false, startsExpr: true, rightAssociative: false, isLoop: false, isAssign: false, prefix: false, postfix: false, binop: null, updateContext: null }, …}和 AST 节点一样它们也有 start,end,loc 属性。2.语法分析语法分析阶段会把一个令牌流转换成 AST 的形式。 这个阶段会使用令牌中的信息把它们转换成一个 AST 的表述结构,这样更易于后续的操作。简单来说,解析阶段就是code(字符串形式代码) -> tokens(令牌流) -> AST(抽象语法树)Babel 使用 @babel/parser 解析代码,输入的 js 代码字符串根据 ESTree 规范生成 AST(抽象语法树)。Babel 使用的解析器是 babylon。什么是AST2.转换转换步骤接收 AST 并对其进行遍历,在此过程中对节点进行添加、更新及移除等操作。 这是 Babel 或是其他编译器中最复杂的过程。Babel提供了@babel/traverse(遍历)方法维护这AST树的整体状态,并且可完成对其的替换,删除或者增加节点,这个方法的参数为原始AST和自定义的转换规则,返回结果为转换后的AST。3.生成代码生成步骤把最终(经过一系列转换之后)的 AST 转换成字符串形式的代码,同时还会创建源码映射(source maps)。代码生成其实很简单:深度优先遍历整个 AST,然后构建可以表示转换后代码的字符串。Babel使用 @babel/generator 将修改后的 AST 转换成代码,生成过程可以对是否压缩以及是否删除注释等进行配置,并且支持 sourceMap。 实践前提在这之前,你必须对Babel有了基本的了解,下面我们简单的了解下babel的一些东西,以便于后面开发插件。babel-corebabel-core是Babel的核心包,里面存放着诸多核心API,这里说下transform。transform : 用于字符串转码得到AST 。传送门//安装npm install babel-core -D;import babel from ‘babel-core’;/ * @param {string} code 要转译的代码字符串 * @param {object} options 可选,配置项 * @return {object} */babel.transform(code:String,options?: Object)//返回一个对象(主要包括三个部分):{ generated code, //生成码 sources map, //源映射 AST //即abstract syntax tree,抽象语法树}babel-typesBabel Types模块是一个用于 AST 节点的 Lodash 式工具库(译注:Lodash 是一个 JavaScript 函数工具库,提供了基于函数式编程风格的众多工具函数), 它包含了构造、验证以及变换 AST 节点的方法。 该工具库包含考虑周到的工具方法,对编写处理AST逻辑非常有用。传送门npm install babel-types -D; import traverse from “babel-traverse”;import * as t from “babel-types”;traverse(ast, { enter(path) { if (t.isIdentifier(path.node, { name: “n” })) { path.node.name = “x”; } }});JS CODE -> AST查看代码对应的AST树结构Visitors (访问者)当我们谈及“进入”一个节点,实际上是说我们在访问它们, 之所以使用这样的术语是因为有一个访问者模式(visitor)的概念。访问者是一个用于 AST 遍历的跨语言的模式。 简单的说它们就是一个对象,定义了用于在一个树状结构中获取具体节点的方法。 这么说有些抽象所以让我们来看一个例子。const MyVisitor = { Identifier() { console.log(“Called!”); }};// 你也可以先创建一个访问者对象,并在稍后给它添加方法。let visitor = {};visitor.MemberExpression = function() {};visitor.FunctionDeclaration = function() {}注意: Identifier() { … } 是 Identifier: { enter() { … } } 的简写形式这是一个简单的访问者,把它用于遍历中时,每当在树中遇见一个 Identifier 的时候会调用 Identifier() 方法。Paths(路径)AST 通常会有许多节点,那么节点直接如何相互关联呢? 我们可以使用一个可操作和访问的巨大可变对象表示节点之间的关联关系,或者也可以用Paths(路径)来简化这件事情。Path 是表示两个节点之间连接的对象。在某种意义上,路径是一个节点在树中的位置以及关于该节点各种信息的响应式 Reactive 表示。 当你调用一个修改树的方法后,路径信息也会被更新。 Babel 帮你管理这一切,从而使得节点操作简单,尽可能做到无状态。Paths in Visitors(存在于访问者中的路径) 当你有一个 Identifier() 成员方法的访问者时,你实际上是在访问路径而非节点。 通过这种方式,你操作的就是节点的响应式表示(译注:即路径)而非节点本身。const MyVisitor = { Identifier(path) { console.log(“Visiting: " + path.node.name); }};Babel插件规则Babel的插件模块需要我们暴露一个function,function内返回visitor对象。//函数参数接受整个Babel对象,这里将它进行解构获取babel-types模块,用来操作AST。module.exports = function({types:t}){ return { visitor:{ } } }撸一个Babel …插件 !!!做一个简单的ES6转ES3插件:1. let,const 声明 -> var 声明 2. 箭头函数 -> 普通函数文件结构|– index.js 程序入口|– plugin.js 插件实现|– before.js 转化前代码|– after.js 转化后代码|– package.json 首先,我们先创建一个package.json。npm initpackage.json{ “name”: “babelplugin”, “version”: “1.0.0”, “description”: “create babel plugin”, “main”: “index.js”, “scripts”: { “babel”: “node ./index.js” }, “author”: “webfansplz”, “license”: “MIT”, “devDependencies”: { “@babel/core”: “^7.2.2” }}可以看到,我们首先下载了@babel/core作为我们的开发依赖,然后配置了npm run babel作为开发命令。index.jsconst { transform } = require(’@babel/core’);const fs = require(‘fs’);//读取需要转换的js字符串const before = fs.readFileSync(’./before.js’, ‘utf8’);//使用babel-core的transform API 和插件进行字符串->AST转化。const res = transform(${before}, { plugins: [require(’./plugin’)]});// 存在after.js删除fs.existsSync(’./after.js’) && fs.unlinkSync(’./after.js’);// 写入转化后的结果到after.jsfs.writeFileSync(’./after.js’, res.code, ‘utf8’);我们首先来实现 功能 1. let,const 声明 -> var 声明let code = 1;我们通过传送门查看到上面代码对应的AST结构为我们可以看到这句声明语句位于VariableDeclaration节点,我们接下来只要操作VariableDeclaration节点对应的kind属性就可以啦before.jsconst a = 123;let b = 456;plugin.jsmodule.exports = function({ types: t }) { return { //访问者 visitor: { //我们需要操作的访问者方法(节点) VariableDeclaration(path) { //该路径对应的节点 const node = path.node; //判断节点kind属性是let或者const,转化为var [’let’, ‘const’].includes(node.kind) && (node.kind = ‘var’); } } };};ok 我们来看看效果!npm run babelafter.jsvar a = 123;var b = 456;没错,就是这么吊!!!功能1搞定,接下来实现功能2. 箭头函数 -> 普通函数 我们先来看看箭头函数对应的节点是什么?let add = (x, y) => { return x + y;};我们通过传送门查看到上面代码对应的AST结构为我们可以看到箭头函数对应的节点是ArrowFunctionExpression。接下来我们再来看看普通函数对应的节点是什么?let add = function(x, y){ return x + y;};我们通过传送门查看到上面代码对应的AST结构为我们可以看到普通函数对应的节点是FunctionExpression。 所以我们的实现思路只要进行节点替换(ArrowFunctionExpression->FunctionExpression)就可以啦。plugin.jsmodule.exports = function({ types: t }) { return { visitor: { VariableDeclaration(path) { const node = path.node; [’let’, ‘const’].includes(node.kind) && (node.kind = ‘var’); }, //箭头函数对应的访问者方法(节点) ArrowFunctionExpression(path) { //该路径对应的节点信息 let { id, params, body, generator, async } = path.node; //进行节点替换 (arrowFunctionExpression->functionExpression) path.replaceWith(t.functionExpression(id, params, body, generator, async)); } } };};满怀激动的npm run babelafter.jsvar add = function (x, y) { return x + y;};惊不惊喜 ? 意不意外 ? 你以为这样就结束了吗 ? 那你就太年轻啦。我们经常会这样写箭头函数来省略return。let add = (x,y) =>x + y;我们来试试 这样能不能转义npm run babelGG.控制台飘红下面我直接贴下最后的实现,具体原因我觉得读者自己研究或许更有趣 plugin.jsmodule.exports = function({ types: t }) { return { visitor: { VariableDeclaration(path) { const node = path.node; [’let’, ‘const’].includes(node.kind) && (node.kind = ‘var’); }, ArrowFunctionExpression(path) { let { id, params, body, generator, async } = path.node; //箭头函数我们会简写{return a+b} 为 a+b if (!t.isBlockStatement(body)) { const node = t.returnStatement(body); body = t.blockStatement([node]); } path.replaceWith(t.functionExpression(id, params, body, generator, async)); } } };};小功告成源码地址如果觉得有帮助到你,请给个star或者follow 支持下作者哈~接下来还会有很多干货哦!!!参考文献很棒的Babel手册 ...

January 14, 2019 · 4 min · jiezi

webpack4系列教程(四):处理项目中的资源文件(一)

Loader的使用之前的博文已经介绍了Loader的概念以及用法,webpack 可以使用 loader 来预处理文件,这允许你打包除 JavaScript 之外的任何静态资源, 甚至允许你直接在 JavaScript 模块中 import CSS文件。在 src 目录下新建 components 文件夹,新建 modal 组件: 编写代码:<!–modal.ejs–><div class=“modal-parent”> <div class=“modal-header”> <h3 class=“modal-title”><%= title %></h3> </div> <div class=“modal-body”> <%= content %> </div> <div class=“modal-footer”> <%= footer %> </div></div>// modal.jsimport template from ‘./modal.ejs’export default function modal () { return { name: ‘modal’, template: template }}修改 main.js:import Modal from ‘./components/modal/modal’const App = function () { let div = document.createElement(‘div’) div.setAttribute(‘id’, ‘app’) document.body.appendChild(div) let dom = document.getElementById(‘app’) let modal = new Modal() dom.innerHTML = modal.template({ title: ‘标题’, content: ‘内容’, footer: ‘底部’ })}const app = new App() 此时执行 npm run build 会报错 : webpack 无法解析 .ejs 文件,因此我们需要安装对应的 loader:npm i ejs-loader -D 并修改 webpack.config.js 添加 module 属性:module: { rules: [ { test: /.ejs$/, use: [’ejs-loader’] } ] }再次执行 npm run build 就不会报错了,打开 dist/index.html : 可以看到我们的 modal 组件已经成功渲染出来了。 2. 处理项目中的CSS文件在 modal.css 中加入样式代码:.modal-parent{ width: 500px; height: auto; border: 1px solid #ddd; border-radius: 10px;}.modal-title{ font-size: 20px; text-align: center; padding: 10px; margin: 0;}.modal-body{ border: 1px solid #ddd; border-left: 0; border-right: 0; padding: 10px;}.modal-footer{ padding: 10px;}安装 css-loader 和 style-loader:npm i css-loader style-loader -D 修改webpack.config.js 中的 module.rules ,添加css-loader 和 style-loader:module: { rules: [ { test: /.ejs$/, use: [’ejs-loader’] }, { test: /.css$/, use: [ ‘style-loader’, ‘css-loader’ ] } ] },在 modal.js 中引入 modal.css:import ‘./modal.css’再次执行 npm run build ,打开 dist/index.html: CSS样式已经通过 style 标签添加到页面上了; 3. 处理项目中的图片 在src目录下创建 assets/img ,放入两张图片 给 modal 添加一个背景图的样式:.modal-body{ border: 1px solid #ddd; border-left: 0; border-right: 0; padding: 10px; background: url("../../assets/img/bg.jpg"); color: #fff; height: 500px;}由于webpack无法处理图片资源,所以也要安装对应的 loadernpm install –save-dev url-loader file-loader在 webpack.config.js 中添加 loader rules: [ { test: /.ejs$/, use: [’ejs-loader’] }, { test: /.css$/, use: [ ‘style-loader’, ‘css-loader’ ] }, { test: /.(jpg|jpeg|png|gif|svg)$/, use: ‘url-loader’ } ]打包代码之后,在浏览器打开 dist/index.html ,可见图片已经显示出来了: 仔细查看这张图片可以发现,它是通过 DataURL 加载出来的: 下面更改 url-loader 的配置,limit表示在文件大小低于指定值时,返回一个DataURL{ test: /.(jpg|jpeg|png|gif|svg)$/, use: [ { loader: ‘url-loader’, options: { name: ‘[name]-[hash:5].[ext]’, limit: 1024 } } ] }再次打包后,图片会以文件形式展示出来: 本人才疏学浅,不当之处欢迎批评指正

January 13, 2019 · 2 min · jiezi

webpack4系列教程(三):自动生成项目中的HTML文件

webpack中的CommonJS和ES Mudule 规范1.1 CommonJs规范CommonJs规范的出发点:JS没有模块系统、标准库较少、缺乏包管理工具;为了让JS可以在任何地方运行,以达到Java、PHP这些后台语言具备开发大型应用的能力。在CommonJs规范中:一个文件就是一个模块,拥有单独的作用域;普通方式定义的变量、函数、对象都属于该模块内;通过require来加载模块;通过exports和modul.exports来暴露模块中的内容;1.2 ES Mudule 规范ES6在语言标准的层面上,实现了模块功能,基本特点如下:每一个模块只加载一次, 每一个JS只执行一次, 如果下次再去加载同目录下同文件,直接从内存中读取;每一个模块内声明的变量都是局部变量, 不会污染全局作用域;模块内部的变量或者函数可以通过export导出;一个模块可以导入别的模块;模块功能主要由两个命令构成:export和import;export命令用于规定模块的对外接口,import命令用于输入其他模块提供的功能:// esm.jslet firstName = ‘Jack’;let lastName = ‘Wang’;export {firstName, lastName}// export命令除了输出变量,还可以输出函数export function (a, b) { return a + b}使用export命令定义了模块的对外接口以后,其他 JS 文件就可以通过import命令加载这个模块,import命令接受一对大括号,里面指定要从其他模块导入的变量名,大括号里面的变量名,必须与被导入模块对外接口的名称相同。// main.jsimport {firstName, lastName} from ‘./esm’;function say() { console.log(‘Hello , ’ + firstName + ’ ’ + lastName)}1.3 使用现在,在src目录下新建 sum.js 和 minus.js// sum.js ES Mudule 规范// export default命令,为模块指定默认输出export default function (a, b) { return a + b}// minus.js commonJS 规范module.exports = function (a, b) { return a - b} 修改 main.js import sum from ‘./sum’import minus from ‘./minus’console.log(‘sum(1, 2): ’ + sum(1, 2))console.log(‘minus(5, 2): ’ + minus(5, 2))执行 npm run build 之后,打开 index.html,在控制台中可以看到输出的结果。 2. 自动生成项目中的HTML文件在前文中我们为了演示打包好的 main.bundle.js ,在根目录下创建了一个 index.html ,并引入main.bundle.js。而在实际项目中,我们可以通过 webpack 的一个插件:HtmlWebpackPlugin 来自动生成HTML文件并引入我们打包好的JS和CSS文件。 安装:npm install –save-dev html-webpack-plugin 整理项目目录:在根目录创建config文件夹,把webpack.config.js移入config,并修改webpack.config.js:const path = require(‘path’)const HtmlWebpackPlugin = require(‘html-webpack-plugin’)const config = { mode: ’none’, entry: { main: path.join(__dirname, ‘../src/main.js’) }, output: { filename: ‘[name].bundle.js’, path: path.join(__dirname, ‘../dist’) }, plugins: [ new HtmlWebpackPlugin({ template: path.join(__dirname, ‘../index.html’), inject: true, minify: { removeComments: true } }) ]}module.exports = configtemplate:模版文件的路径,这里使用根目录下的index.html文件;inject:设为 true 表示把JS文件注入到body结尾,CSS文件注入到head中;minify:removeComments: true 表示删除模版文件中的注释,minify还有很多配置可选请自行参阅;下一步注释掉index.html 中我们手动引入的 script :<!DOCTYPE html><html lang=“en”><head> <meta charset=“UTF-8”> <meta name=“renderer” content=“webkit”/> <meta http-equiv=“X-UA-Compatible” content=“IE=edge,chrome=1”/> <title>Title</title></head><body><!– <script src=“dist/main.bundle.js”></script> –></body></html>执行 npm run build ,可以看到,dist 目录下多了一个 index.html,这就是通过 HtmlWebpackPlugin 生成的文件,打开dist/index.html,已经自动引入了 main.bundle.js并且注释已被删除。 至此,我们已经成功实现自动生成项目中的HTML文件了。3. 清理/dist文件夹每次执行npm run build 打包时,都会有上次的代码遗留下来,导致我们的 /dist 文件夹相当杂乱。通常,在每次构建前清理 /dist 文件夹,是比较推荐的做法。clean-webpack-plugin 是一个比较普及的管理插件,让我们安装和配置下:npm install clean-webpack-plugin –save-dev在webpack.config.js 中使用:const CleanWebpackPlugin = require(‘clean-webpack-plugin’)在 plugins 中加入:new CleanWebpackPlugin([‘dist’],{root: path.join(__dirname, ‘../’)})第一个参数表示文件夹路径数组;第二个参数是 options 配置项,root 为到webpack根文件夹的绝对路径,默认为 __dirname,由于dist文件夹和webpack.config.js不再相同目录下,因此我们需要重新定义 root 路径,以免无法找到 dist 文件夹。执行 npm run build ,在命令行中可见: dist 文件夹已被删除了。本人才疏学浅,不当之处欢迎批评指正

January 13, 2019 · 2 min · jiezi

webpack4系列教程(二):创建项目,打包第一个JS文件

创建项目1.1 初始化一个项目首先安装nodejs,打开 nodeJs官网 直接下载安装即可,安装完毕后打开命令行工具,进入你的项目文件夹,执行npm init 进行项目的初始化:过程中会让你填写项目名、版本、描述、仓库地址、关键字等信息,可以不填一路回车,执行完毕后会在根目录下创建一个 package.json 文件,这样就初始化结束了。1.2 安装webpack由于在webpack4中已经不再默认安装 webpacl-cli,所以我们要手动安装,在命令行执行 npm i webpack webpack-cli -D 即可。对于大多数项目,建议本地安装。这可以使我们在引入破坏式变更(breaking change)的依赖时,更容易分别升级项目。2. 打包第一个JS文件 首先,我们在根目录下创建一个 webpack.config.js 文件和一个src文件夹。然后在src中创建一个 main.js 文件,如下:在 main.js 中写一行 alert(‘hello world’)然后打开 webpack.config.js ,进行webpack的配置:const path = require(‘path’)let config = { mode: ’none’, entry: { main: path.join(__dirname, ‘./src/main.js’) }, output: { filename: ‘[name].bundle.js’, path: path.join(__dirname, ‘./dist’) }}module.exports = config我们设置了一个名为 main 的入口,并以 src 下的 main.js 作为入口文件,然后输出到根目录下的 dist 文件夹中。在webpack4中,我们需要设置 mode 属性,用来决定当前是development还是production环境,webpack会根据此值来进行一些默认操作,两种环境的不同配置后面的博文会详解,这里我们设置为 ’none’ ,来避免默认操作。前文已经说过,path 是 nodeJs中的核心模块用来操作路径,__dirname 表示文件的当前路径(此时为根路径)。而 output中的filename属性,[name] 表示入口的名称,此处就是 main。接下来打开 package.json 文件,来编写一条命令执行webpack的打包。在 script 中添加:“build”: “webpack –config webpack.config.js –progress –colors"webpack –config path/to/your/file/file.js 表示执行某个配置文件,–progress可以让我们看到打包的进度 , –colors 开启命令行颜色显示,更多的webpack命令参数大家可以另行查阅。然后就可以在命令行执行:npm run build,执行完毕后,我们可以看到,在根目录下多了一个 dist 文件夹 并有一个 main.bundle.js文件,这就是webpack为我们打包出来的静态资源,而文件路径就是我们在 output 中设置的值。为了演示打包好的 main.bundle.js ,我们在根目录下创建一个 index.html ,并引入main.bundle.js<!DOCTYPE html><html lang=“en”><head> <meta charset=“UTF-8”> <title>Title</title></head><body><script src=“dist/main.bundle.js”></script></body></html>在浏览器中打开 index.html,可见main.js中的代码已经被执行了:在IDE中打开main.bundle.js,代码的最底部可以看到我们在main.js中写的代码。至此,我们的第一次 webpack 打包就成功了。本人才疏学浅,不当之处欢迎批评指正

January 12, 2019 · 1 min · jiezi

构建多页面应用——优化(二)

最近,一直尝试使用webpack做多页面应用的开发。并且一个实际的项目为原型,实现对一个静态的企业站进行优化。原站点地址,测试站点地址。如果想要做一个自己个个人博客,或者企业官网来说,有一定的参考意义。webpack的resolve.alias在做模块化开发的过程中,有一个需要解决的问题就是引用模块的路径问题。注:在webpack中,每一个文件(不管是js,css, html,还是图片等)都被称之为一个块。为了实现模块化,细粒度化的控制,往往会将代码块分成为不可分割的块,这样做虽然方便了管理控制,但是也会造成项目的文件嵌套很严重,再饮用的时候需要格外小心路径,同时也会造成开发者的负担(抛开其他不讲,但从技术角度来说,对于开发人员来说,能用一行代码解决的问题,绝不用两行,能少输入一个单词就少输入一个)。而webpack的resolve.alias可以为指定路径的字符串起别名。在本文所使用的示例,这样定义别名:…resolve: { alias: { ‘@’: path.join(__dirname, ‘..’, ‘pages/’), // 根目录 ‘@css’: path.join(__dirname, ‘assets/css/’), // css ‘@img’: path.join(__dirname, ‘assets/imgs/’), // picture // ‘@font’: path.join(__dirname, ‘assets/fonts/’), ‘@data’: path.join(__dirname, ‘pages/data/’), // mock data ‘@utils’: path.join(__dirname, ‘pages/utils/’) // snippets code }},…当然,上面的别名并不是万能的,有一个问题就是background-iamge 和 font-face 的使用url()会有一些问题,url()中的路径必须是字符串,暂时没有好的办法解决。但是使用sass,可以定义变量,可以通过变量来指定路径,但是要严格控制引用变量模块的文件的目录,在本文所使用的示例中,统一将应用变量文件assets/css/path.scss的文件,控制在两个层级。具体可参考所提供源代码中的具体使用。模拟数据实际的项目没有使用任何一种语言的后端代码,更不用说数据库。全部使用的是模拟数据。为了方便管理维护项目的模拟数据,将项目的所有数据统一整理到了示例的pages/data目录下。静态资源图片的处理第一优化的时候,就简单的讲了下,如何使用imagemin提供的插件,来实现对常见类型(.jpg,.png,.gif)图片的处理。第一种引用图片的方案之前做单页面应用开发的时候,喜欢将所有的图片优化处理后统一放在一个目录中,然后将它们放在服务器中,最后在开发或生产环境中,使用绝对路径进行访问。这种方式的好处是不用担心相对路径造成的路径问题。但是缺点是,操作起来不方便,尤其是开发环境。因为你不知道项目究竟要使用多少的静态资源,尤其是使用哪种静态资源。这种方式在团队合作的项目中,比较常见,但是对于提升团队的效率并不明显。第二种引用图片的方案所以,对于开发者来说,如果如果需要什么静态资源,就放在自己的本地目录,这样可以随心所欲的添加。在本文所采用的示例中,我做了一些尝试,将所有的图片资源进行了分类。需要转化为base64的图片放一个文件夹assets/imgs/base64/,需要合成雪碧图的单独放在一个文件夹;assets/imgs/sprites/,为了方便管理合成不同雪碧图的源图片,我又在该目录下创建了子文件夹;而对于<img src="…" />要引用的图片的存放使用了两个文件夹,assets/imgs/static存放了未经优化的所有的图片,而目录assets/imgs/others,存放了所有优化过的图片(包含两部分,一部分是使用npm run img命令优化的assets/imgs/static目录下的图片,另一部分是npm run dev命令优化的雪碧图图片,它的前缀带有-sprite这样的后缀)。这种方案,使用的是相对路径应用图片。可参考pages/data/contactus.js文件的代码:const loadImg = require(’@utils/load-img’)module.exports = { cn_name: ‘联系我们’, en_name: ‘CONTACT US’, img: loadImg(‘second/contactus-tag.png’), …}而工具代码片段loadImg的代码如下:module.exports = function(str) { return require(’@img/other/’ + str)}源代码webpack4.x multi-page构建多页面应用系列文章构建多页面应用构建多页面应用——单个页面的处理构建多页面应用——模板构建多页面应用——静态资源构建多页面应用——优化(一)构建多页面应用——hash构建多页面应用——优化(二)

January 7, 2019 · 1 min · jiezi

Webpack + Vue 多页面项目升级 Webpack 4 以及打包优化

前言早在 2016 年我就发布过一篇关于在多页面下使用 Webpack + Vue 的配置的文章,当时也是我在做自己一个个人项目时遇到的配置问题,想到别人也可能遇到跟我同样的问题,就把配置的思路分享出来了,传送门在这里。因为那份配置直到现在还有人在关注,同时最近公司帮助项目升级了 Webpack 4,趁机也把之前的配置也升级了一下,而且博客荒废了这么久,都快 9102 年了,不能连年均一篇博文都不到,所以有了下面的分享。下面的配置主要是给在多页面下使用 Webpack 的同学在升级 Webpack 时提供一点思路,多页面的配置思路请点击上面的传送门。下面代码的地址 https://github.com/cnu4/Webpack-Vue-MultiplePage1. Webpack 升级 4.x1.1. 升级和安装相关依赖webpack 升级webpack-cli webapck4.x 需要新加的依赖mini-css-extract-plugin 取代 extract-text-webpack-plugin其他相关 loader 和 plugincss-loaderfile-loaderurl-loadervue-style-loadervue-template-compiler(注意要保持与 vue 版本一直)html-webpack-plugin@next1.2 修改配置mode 构建模式设置 mode 构建模式,比如 development 会将 process.env.NODE_ENV 的值设为 developmentmini-css-extract-plugin删除原 extract-text-webpack-plugin 配置,增加 mini-css-extract-plugin 配置module.exports = { plugins: [ new MiniCssExtractPlugin({ filename: ‘css/[name].css’ }), ],}module.exports = { module: { rules: [ { test:/.vue$/, loader: ‘vue-loader’, }, { test: /.css$/, use: [ // 开发模式下使用 vue-style-loader,以便使用热重载 process.env.NODE_ENV !== ‘production’ ? ‘vue-style-loader’ : MiniCssExtractPlugin.loader, ‘css-loader’ ] }, ] }}optimization这是 webpack 4 一个比较大的变动点,webpack 4 中删除了 webpack.optimize.CommonsChunkPlugin,并且使用 optimization 中的splitChunk来替代,下面的配置代替了之前的 CommonsChunkPlugin 配置,同意能提取 JS 和 CSS 文件module.exports = { optimization: { splitChunks: { vendors: { name: ‘venders’, chunks: ‘all’, minChunks: chunks.length } }}vue-loader 升级vue-loader 15 注意要配合一个 webpack 插件才能正确使用const { VueLoaderPlugin } = require(‘vue-loader’) module.exports = { plugins: [ new VueLoaderPlugin() ]}html-webpack-plugin 升级升级到 next,否则开发下无法正常注入资源文件文件压缩optimize-css-assets-webpack-pluginterser-webpack-plugin压缩的配置也移动到了 optimization 选项下,值得注意的是压缩工具换成了 terser-webpack-plugin,这是 webpack 官方也推荐使用的,估计在 webpack 5 中会变成默认的配置,实测打包速度确实变快了很多。配置module.exports = { minimizer: [ new TerserPlugin({ // 压缩js cache: true, parallel: true } }), new OptimizeCSSAssetsPlugin({ // 压缩css cssProcessorOptions: { safe: true } }) ] }}2. 增加 ES6+ 支持2.1 安装依赖"babel-core": “^6.26.3”,“babel-loader”: “^7.1.5”,“babel-plugin-transform-runtime”: “^6.23.0”,“babel-preset-env”: “^1.7.0”,“babel-preset-stage-2”: “^6.24.1”,“babel-runtime”: “^6.26.0”,2.2 添加配置文件 .babelrc{ “presets”: [ [“env”, { “modules”: false, “targets”: { “browsers”: ["> 1%", “last 2 versions”, “ie >= 9”] }, “useBuiltIns”: “usage” }], “stage-2” ], “plugins”: [“transform-runtime”]}2.3 增加 webpack 配置module.exports = { modules: { rules: [ { test: /.js$/, loader: ‘babel-loader’, exclude: /node_modules/ } ] }}2.4 更新 eslint 配置3. 打包速度优化可以使用下面的插件看看打包时间主要耗时在哪speed-measure-webpack-plugin3.1 TerserPlugin 开启 parallel 选项开启多线程3.2 HappyPack 和 thread-loader 开启 Loader 多进程转换github 的 Demo 中没有引入,有兴趣的同学可以尝试,在一些耗时的 Loader 确实可以提高速度const HappyPack = require(‘happypack’);exports.module = { rules: [ { test: /.js$/, // 1) replace your original list of loaders with “happypack/loader”: // loaders: [ ‘babel-loader?presets[]=es2015’ ], use: ‘happypack/loader’, include: [ /* … / ], exclude: [ / … */ ] } ]};exports.plugins = [ // 2) create the plugin: new HappyPack({ // 3) re-add the loaders you replaced above in #1: loaders: [ ‘babel-loader?presets[]=es2015’ ] })];3.3 提前打包公共代码DllPlugin使用 DllPlugn 将 node_modules 或者自己编写的不常变的依赖包打一个 dll 包,提高速度和充分利用缓存。相当于 splitChunks 提取了公共代码,但 DllPlugn 是手动指定了公共代码,提前打包好,免去了后续 webpack 构建时的重新打包。首先需要增加一个 webpack 配置文件 webpack.dll.config.js 专门针对 dll 打包配置,其中用到 webpack.DllPlugin。执行 webpack –config build/webpack.dll.config.js 后,webpack会自动生成 2 个文件,其中vendor.dll.js 即合并打包后第三方模块。另外一个 vendor-mainifest.json 存储各个模块和所需公用模块的对应关系。接着修改我们的 webpack 配置文件,在 plugin 配置中增加 webpack.DllReferencePlugin,配置中指定上一步生成的 json 文件,然后手动在 html 文件中引用上一步的 vendor.dll.js 文件。后面如果增删 dll 中的依赖包时都需要手动执行上面打包命令来更新 dll 包。下面插件可以自动完成这些操作。AutoDllPlugin安装依赖 autodll-webpack-pluginAutoDllPlugin 自动同时相当于完成了 DllReferencePlugin 和 DllPlugin 的工作,只需要在我们的 webpack 中添加配置。AutoDllPlugin 会在执行 npm install / remove / update package-name 或改变这个插件配件时重新打包 dll。需要注意的是改变 dll 中指定的依赖包不会触发自动重新打包 dll。实际打包中生成环境是没问题的,但开发模式下在有缓存的情况下,autodll 插件不会生成新的文件,导致 404,所以在 Demo 中暂时关了这个插件。不过 dll 提前打包了公共文件,确实可以提高打包速度,有兴趣的同学可以研究下开发模式下的缓存问题,欢迎在评论中分享。module.exports.plugins.push(new AutoDllPlugin({ inject: true, // will inject the DLL bundles to html context: path.join(__dirname, ‘.’), filename: ‘[name].dll.js’, debug: true, inherit: true, // path: ‘./’, plugins: [ new TerserPlugin({ cacheL true, parallel: true }), new MiniCssExtractPlugin({ filename: ‘[name].css’ }) ], entry: { vendor: [‘vue/dist/vue.esm.js’, ‘axios’, ’normalize.css’] }}));3.4 terser-webpack-pluginwebpack 官方推荐使用的 JS 压缩插件,取代 UglifyJS,大幅提高打包速度4. 其他问题下面是我公司项目中遇到的问题,各位升级过程中如果遇到同样的问题可以参考一下解决思路。4.1 json-loaderjson-loaderwebpack4 内置的json-loader 有点兼容性问题,安装 json-loader 依赖和更改配置解决:{ test: /.json$/, //用于匹配loaders所处理文件拓展名的正则表达式 use: ‘json-loader’, //具体loader的名称 type: ‘javascript/auto’, exclude: /node_modules/}4.2 vue-loadervue-loader 升级到 15.x 后,会导致旧的 commonjs 写法加载有问题,需要使用 require(‘com.vue’).default 的方式引用组件13的版本还可以设置 esModule,14 以后的版本不能设置了,vue 文件导出的模块一定是 esModule解决:使用 require(‘com.vue’).default 或者 import 的方式引用组件esModule option stopped working in version 14 · Issue #1172 · vuejs/vue-loader · GitHub尤大大建议可以自己写一个 babel 插件,遇到 require vue 文件的时候自动加上 default 属性,这样就不用改动所有代码,我们在项目中也是这样处理的。4.3 提取公共 css 代码scss 中 import 的代码不能被提取到公共 css 中解决:改到 js 中引入就可以,详见下面 issuemini-css-extract-plugin + sass-loader + splitChunks · Issue #494.4 mini-css-extract-plugin filename 不支持函数mini-css-extract-plugin 的 filename 选项不支持函数,所以只能转用其他方式解决解决:使用插件 FileManagerPlugin 在构建后移动文件,等 filename 支持函数后再优化feat: allow the option filename to be a function · Issue #143 · webpack-contrib/mini-css-extract-plugin · GitHub

December 30, 2018 · 3 min · jiezi

aotoo-hub,一体式大前端架构

年底了,开源一套我们的大前端架构aotoo-hub,小伙伴们都用得很爽的。 GITHUB – 听说star的人明年会发财 文档aotoo-hub是一套正式上线的大前端解决方案。迭代的这2年多的时间,从webpack-1熬到了webpack-4,从纯前端脚手架到融合node端的整体方案,从繁复到精简,重构的次数不要太多。简单、易用、易部署的一体化大前端开发体验是aotoo-hub始终的追求,我们不是在重构,就是在重构的路上(保持一致性)。aotoo-hub是一套前端、node端彼此相亲相爱不分离,你中有我,我中有你的大前端解决方案。前端负责静态资源编译与分享,node端负责服务、路由与渲染,模板则起到桥接两端的作用(结构、样式、渲染),最终http服务将渲染完成的模板投送到客户端浏览器上。完整的大前端方案需要解决前端、node两端各自的开发、部署难的问题,并且需要将两端融合为一套有机的整体,同时还能兼顾到工程化实现。aotoo-hub开发迭代的过程中我们始终秉持着下面这些原则通用性尽量多的支持多种开源框架,使我们能够为不同业务选择合适的开源框架。aotoo-hub现在支持react,vue,小程序(alpha),未来也许能够加入app的相关框架,比如RN或者FLUTTER?融合性前端与node的有机融合不止是一种更好的体验,同时前端、node端能够共享静态资源,部署同构组件,简化resful的路由等,一体化的设计使得项目的开发、部署、维护都变得简单且易于维护。也许你会用到egg,nest等node框架作为后端支撑,maybe更好的方案是java, go, php等的框架。易用性aotoo-cli是专门为aotoo-hub打造的一套命令工具,使得aotoo-hub更容易上手了,还是写写code演示一下启动默认项目# 安装aotoo-cli$ npm install -g aotoo-cli # aotoo -V检验是否安装成功# 新建workspace$ aotoo init oneWorkspace #创建目录oneWorkspace,并初始化项目环境# 启动默认项目 $ cd oneWorkspace$ aotoo dev新建项目# 安装aotoo-cli$ npm install -g aotoo-cli # aotoo -V检验是否安装成功# 新建workspace$ aotoo init oneWorkspace #创建目录oneWorkspace,并初始化项目环境# 新建项目$ cd oneWorkspace$ aotoo create newProject # 创建一个项目,名称为newProject# 启动项目开发版本$ aotoo dev newProject # then open browse http://localhost:3000# 编译项目$ aotoo build newProject # 静态资源会cdn化# 启动生产项目$ aotoo build newProject$ aotoo start newProject # 使用node启动$ pm2 start index.js – –start newProject # 使用pm2启动生产项目对吧,命令行应该不算复杂。好了,这里大概对aotoo-hub进行了一些介绍,接着和大家说说创建项目的流程及初始化项目的文件构成准备支撑系统mac osxlinuxwindows,主要是/和\的问题全局环境node-gypnode-pre-gyp$ npm install -g node-gyp$ npm install -g node-pre-gyp一、新建workspace新建workspace其实就是一个准备编译环境的过程,我们会准备编译文件,项目目录,项目配置文件# 新建命名空间$ aotoo init wp-1aotoo.config.jsaotoo-hub的配置文件,可以在这里设置项目初始目录,版本号等等配置信息,配置内容大致如下const path = require(‘path’)const pakg = require(’./package.json’)const ROOT = __dirnameconst version = pakg.versionmodule.exports = { // 版本信息,由package.json的version来指定 // 默认情况下,所有项目产出的版本号都会依据这个version值 // 版本信息会被用于生成dist下的版本目录 version: version, // node的环境变量NODE_ENV mode: process.env.NODE_ENV, // workspace的根目录地址 // 会用在aotoo安装插件时,及node端(目录层级很深)掉用 ROOT: ROOT, // 所有项目的原始根目录 src : path.join(__dirname, ‘src’), // 配置默认项目信息 // 小程序项目必须使用这个配置 // 当我们不使用start, name等命令选项时,aotoo-hub会查找该属性下startup为true的项目,并尝试启动 // 当我们配置好默认项目后,命令行可以简化projectName apps: [ { // 项目名称,与src项目项目目录一致 // 任何项目都必须有自己唯一的名称 name: ‘aotooSample’, // 是否启动该项目 startup: true, // 指定项目源源目录 src: path.join(ROOT, ‘src/aotooSample’), // 默认静态资源输出地址为 src/dist // 这里可以手动指定希望输出的目录 // dist // 指定项目端口地址 // 指定项目端口,可为null,系统自动分配端口地址 port: 8400 } ]}build目录包含所有的编译文件src目录src是默认aotoo-hub的源目录,所有新建项目都会在次目录下生成项目文件夹aotooSmple目录是我们的一个demo项目,是aotoo-hub的默认项目,以供参考# 启动默认项目,开发模式$ aotoo dev二、新建项目下面我们开始新建一个项目$ cd wp-1$ aotoo create newProject项目初始目录完整项目目录初始目录是一个精简版的项目,保留了最基础的文件及目录,完整目录如下 wp-1 └── src └── newProject ├── component //组件目录 │ └── …… ├── ssr/sync // 同构模块目录 │ └── …… ├── dist // 静态文件输出目录 │ └── …… ├── js // 前端业务js目录 │ └── index.js ├── css // 前端业务css目录 │ └── index.styl ├── html // 前端业务模板目录,一般的模板都会自动生成,如需要自定义幕版,则根据同名规则自定义生成相关模板 │ └── index.html └── server // node端的源码目录 │── pages // koa2的control层目录 │ └── index.js └── plugins // 自定义插件目录,适用于node端 └── ……注意所有以下划线开始的文件、目录在编译时会被忽略,如_abc/或者_abc.jsconfigs目录项目环境配置文件夹,存放多个环境配置文件,如测试1,测试2,生产等环境配置,所有环境配置在应用是会与公共的default.js配置文件合并js目录存放公共JS,业务JS目录vendors目录公共JS,公共CSS,自动被模板引入。我们将公共JS分为两个部分vendors.js,common.js,公共CSS只有一个common.css vendors.js: 主要内容为框架源码,如react, vue, react-router等 common.js: 根据业务JS由webpack自动生成 common.css: vendors.js中引入的*.styl(aotoo-hub只支持stylus),webpack会将其分离成common样式,该文件也会被模板自动引入 dist: 编译生成 dist//js/vendors.jsjs/*.js 所有的模板,样式自动生成的依据,因为js目录下的所有文件都被当成独立的业务JS文件,会被各个业务页面自动调用 dist: 编译生成 dist//js/.jshtml目录 非必要目录,主动生成,aotoo-hub会自动生成模板文件(依据js/.js),并包含一个id=root的div。 特殊模板需求,请依照*.js的同名文件新建,如src/../js/abc.js对应src/../html/abc.html dist: 编译生成 dist//html/.htmlcss目录 非必要目录,被动生成,aotoo-hub会自动生成样式文件(依据js/.js引入的stylus文件) dist: 编译生成 dist//css/.csscomponent目录 非必要目录,组件存放目录,一个别名目录,我们在node端,前端可以方便引入组件 import someComponent from ‘compoent/someComponent’ …sync目录 非必要目录,同构业务模块存放目录,一个别名目录,我们在node端,前端可以方便引入组件 import someMoudle from ‘sync/someMoudle’ …server node端服务文件 aotoo-hub的node端基于开源库aotoo-koa-server实现。 默认新建的项目是一个纯前端项目,但某些项目有SEO需求,需要我们启动node端来渲染页面 # 带node端启动项目 $ aotoo dev newProject –server新项目有默认的demoindex页面,新项目的node端会自动帮你把所有的node端需要的环境搭建好,同时创建了pages/index.js这个默认的demoindex页面configs.js 这个文件每次启动时会根据src/newProject/configs/目录下的环境配置自动创建,因此如果修改配置请移步src/newProject/configs/中index.js & lib.js aotoo-hub为你将server环境都配置在lib.js中,如果你需要扩展配置,如使用新的koa2的插件,建议修改index.js文件,参考lib.js的写法pages/.js 这里是node端业务js,与src/js/.js对应同一个业务,且同名,如src/newProject/js/abc.js => server/pages/abc.js // server/pages/abc.js // 该文件为koa2框架MVC中的contro层文件 // aotoo-hub接管了渲染方法,因此你只需返回渲染所需的数据部分,oridata / * * oridata {JSON} 系统传递变量,用于渲染模板,需要return oridata * ctx {context} koa2的ctx变量 * * * get: method = GET * post: method = POST * put: method = PUT / module.exports = function (oridata) { return { get: function name(ctx) { oridata.title = ‘aotoo-hub 多项目全栈脚手架’ oridata.root = ‘123’ ctx.redirect(’/docs’) // return oridata }, post: function name(ctx) { return oridata } } }dist 前端静态资源编译后的文件存放位置 1.0.3/ 版本目录,根据aotoo.config中的版本信息 dev/ 该项目处于开发模式,生产模式使用/pro目录 * html/ html模板编译输出目录 * mapfile.json 资源映射文件 三、启动项目单项目启动# 开发编译,并启动前端$ aotoo dev newProject# 如果需要node端(该命令一次生效,终生有效,且后续启动时不需要参数 --server)# 开发编译,并启动Node端$ aotoo dev newProject –server# 生产编译$ aotoo build newProject# 只启动node端(编译完成)$ aotoo start newProject# 带环境编译或启动$ aotoo start newProject –config env_test多项目启动生产环境支持多开项目,且会为每个项目自动分配端口(未指定),开发模式则受制于nodemon对多开不友好的功能,会报错(pm2替代就可以),启动多开也很简单,可以参考上面aotoo.config.js的配置,将startup: false设置为 startup: true就好了,启动时不用指定项目名称,如aotoo dev,或者指定一组项目名称,如:aotoo dev --name aaa --name bbb 今天对aotoo-hub有一个大概的介绍,有问题请提issue,鉴于本人有社交懒癌,问题不一定能及时回答,平时毕竟工作有点多. ...

December 28, 2018 · 2 min · jiezi

“被狗啃”的按钮引发的开源社区信任危机

转载请注明出处:葡萄城官网,葡萄城为开发者提供专业的开发工具、解决方案和服务,赋能开发者。本文由葡萄城技术团队于博客园原创并首发昨天,在国外民众还在欢度圣诞期间,开发者社区却对 Antd 开发团队发起了连番的炮轰。一觉醒来,他们发现由自己参与设计的、公司内网、办事系统等网页上,有一些按钮的上面多了一团白色的“积雪”,在白背景下,看着有点像是被“咬掉”了一块似的,不仅如此,将鼠标指向变化了的按钮时,原本设定好的文字说明也统一变成了“Ho ho ho”,这是“圣诞老人”标志性的笑声。原因是库中暗藏了一个未事先告知的圣诞彩蛋、而且也没法手动禁用,导致许多项目方遭遇了客户投诉。从评论来看,网友们的情绪普遍比较激动,不少人表示“老板问我按钮为什么被狗啃了”、“今天的工作就是给客户们解释我们代码没有被注入”。软件彩蛋的本意是指常用软件当中隐藏了一些小东东,这些小东东我们称之为复活节彩蛋。复活节彩蛋的内容包含的很广,从单纯的列出开发人员名单到各类小游戏都有,但它们都有一个共同的特点就是用户是可控的。我们常用的软件如Windows、Office以及我们用来上网的浏览器IE中都有这样的彩蛋。但号称专注于企业级应用的一个UI库,那么不严肃。敢问哪个老板喜欢这样的“惊喜”?随意调侃节日的UI库,除了这次的圣诞,后面还会不会有元旦、春节、劳动节?事件出现后,在代码托管网站 GitHub 和社交媒体上,开发者们及吃瓜群众表现出了一边倒批评的态度 ——“我留意到按钮组件的上方出现了一块雪花?这是圣诞节彩蛋吧?为什么不经过开发者的允许就擅自加上了?”有人说虽然自己觉的很可爱,但这样的彩蛋不可取有说自己是XX委的,没丢工作还得多亏了自己的小姨子有人说自己看到彩蛋很惊吓,有种失控的感觉也有吐槽英文拼写错误的… AntD这套框架是由支付宝母公司蚂蚁金服设计团队制作的一套开源的前端框架。据蚂蚁金服设计团队今年9月的数据,Ant Design 1.0在发布之后的8个月中,就已经在 GitHub 上收获了 11686 个”星标“。这意味着至少有这些数量的程序员关注,并使用了AntD到自己的项目上。就是这一得到业界广泛关注和使用的基础组件,突然在毫无预警,也没有功能开关的前提下,给所有使用者的按钮控件”下了一场雪“——而且还是在”生产环境“中,不出意外,今天这个彩蛋自己就能自行消失,如果仍然没有消失,请查看作者提供的修复方式:https://github.com/ant-design…AntD是一个采用了 MIT 许可协议的开源项目,所以开发者并不需要为受到损失的开发者承担任何责任。既然做开源项目放到 GitHub 上就是赚个吆喝,不为盈利,也不承担法律责任,那么很显然,“信任”就是一个项目最为金贵的核心价值。开源软件的每一个细节,都暴露在“阳光之下”,只要有心,人们都可以发现。所以理论上任何一点想拿开源产品“图谋不轨”的做法都不会得逞。但是,今天这起事件的发生,又反映了什么?为什么理应有人看到并监督的问题,却安静地躺了两个月都没人发现?开源社区持续出现类似的问题,信任危机也许也只是刚刚开始。后续也许会再次发生此类事件,开发者除了需要认真检查每次的更新外,自己造轮子也可以杜绝这样的问题,但幸亏在这个时代葡萄城有着众多真正出色的企业级商业软件,相信选择使用葡萄城成熟的商业软件:SpreadJS、WijmoJS 是一个更不错的选择,毕竟一家优秀的厂商会为开发者承担所有可能出现的风险这点很重要。最后,让我们祝福他们做的更好。【作者推荐】前端开发工具包 - WijmoJSWijmoJS 前端开发工具包由多款高效、灵活的纯前端控件组成,全面支持 Angular、React、Vue、TypeScript、Knockout 和 Ionic 框架,用于快速搭建企业级桌面/移动 Web 应用程序。WijmoJS 可灵活应对客户需求变化,缩短新项目研发周期,高效处理海量用户数据,提升产品核心竞争力。借助葡萄城深厚的技术底蕴,WijmoJS 为各领域用户提供更稳定、更高效的前端开发工具,帮助中国招商银行、微软、思科、特斯拉、富士通等知名企业快速搭建其 Web 应用程序。WijmoJS 凭借先进的触控设计、全面的框架支持、顶级的控件性能、零依赖的产品特性和易用、轻松的操作体验,全面满足前端开发所需,已成为构建企业 Web 应用程序最高效的纯前端开发工具包。

December 26, 2018 · 1 min · jiezi

[译]635000 个 npm 包中我应该用哪个

[[原文] Which of the 635000 npm modules do I choose? - Corey Cleary](https://www.coreycleary.me/wh…原创翻译,发表于个人博客He Xing’s waking life如有谬误,恳请指正如果您曾在 Node 或 JavaScript 前端开发中投入过时间和精力,那么您就知道 npm 中有数以十万计的模块可供您选择开发者不停的寻求帮助/抱怨:“对模块的选择困难正在蚕食我们"“X 模块和 Y 模块有什么区别?哪一个更好?““npm 很棒,但是这些模块可能在一年半载后失效,取决于模块维护者是否积极"通常在提出这样的问题时,您会得到十个不同的答案。每个人都会给您推荐自己喜欢的模块,接下来就演变成争论哪一个是最好的。选择 npm 模块时很容易面临纸上谈兵。选择太多,而新来者在鼓吹“快上车”,为您的项目选择合适的 npm 模块可能是有难度的。而且这些模块中有许多做类似(或相同)的事情,这也没有帮助。与其浪费时间在 google 上搜索,在 npmjs.org 上搜索,或者浪费更多的时间不构建您的项目,还不如搞清楚什么时候该选择哪些模块。精选清单为了帮助解决这个问题,您将在下面找到针对最常见问题类型(即 web 框架、模板、身份认证等)的 npm 模块列表,以及何时使用这些模块。这里有一些注意事项:您可能熟悉其中一些模块,甚至许多模块,但是有时候您面对的是您还没有接触到的技术栈(可能是身份验证或 Websocket 之类的东西),您需要知道有哪些备选模块可以完成这项工作。您可能有您认为更好的模块,或者可能有一个用例/需求没有包含在这里。我没有列出相同类别的 10 个不同模块,而是缩小了范围,这样您就可以避免分析瘫痪的陷阱。如果您认为自己的用例未被涵盖,请务必自行研究解决。本清单的目的在于让您能更快地启动和运行。这些模块的选择依据如下:它们完成工作的能力如何社区规模(对于支持/故障排除很重要)积极维护如果您发现自己仍然没有足够的信息做出决定,我建议使用slant.co和nodejs.libhunt.com来帮助进行比较。注意:为了保持范围的合理性,这些模块都考虑到了服务器端。它们中的一些可以同时在客户机或服务器上使用,但我的原则是“服务器优先”。HTTP requests (HTTP 请求)Request:当您需要基于回调的 HTTP 请求时可选择它,例如从一个 REST 服务连接到另一个。Axios当您需要基于 Promise的 HTTP 请求时可选择它注意:您可以使用request-promise,但是 axios 的依赖更少,并且基于原生 PromisesWeb frameworks (Web 框架)Express:如果您想为 API、网站或单页应用程序使用轻量级 web 框架,请使用它您不介意使用回调作为默认的异步处理方式使用该框架的模块生态极为繁荣您需要一个支持和故障排除的大型社区Koa:当您想要一个比 Express 更简洁的框架时使用Koa 更像是一个中间件层,它不提供模板或开箱即用的路由,因此更适合 API 开发要想支持开箱即用,您需要 async / awaitHapi如果您想要一个比 Express 或 Koa 更“自带电池”(译者注:原文"batteries"意为您不必重复造轮子,大多数您需要的功能都能通过(已有)库完成。您能导入并使用它们。)的框架,但又不像 Sails 那么多,那就使用它Sails当您需要像 Rails 这样的东西时,请使用它,它具有几乎所有功能(但是根据您的应用程序可能不需要那么多)Validation (前端验证)Ajv在需要验证 JSON 时使用(比如来自 web 请求)您希望与应用程序的其他非 JS 部分共享这些验证规则(因为它是 JSON,所以您可以这样做)Joi在需要验证输入时使用,并且喜欢链式调用的风格(译者注:代码见下方),而不是在 JSON 中定义验证规则您正在使用 Hapi(Hapi 自带 Joi)const schema = joi.object().keys({ id: joi.string().guid().required(), username: joi.string().alphanum().min(8).required()});Authentication (身份认证)Passport:当您需要为您的网站或 API 使用身份验证中间件时使用您希望能够在多种身份验证类型(Oauth,Facebook 等)之间进行选择您需要管理会话Asynchronous (异步)Async (library):当您需要使用旧版本的 Node,而该版本的 Node 支持只支持回调而不支持 Promises 时ES6 原生 promises (原生 JS, 非 npm):在使用大于 0.12 的 Node 版本时使用另一件需要考虑的事情是您的团队对 Promises 的接受程度。在 2018 年,大多数开发人员应该没问题了async / await(原生 JS,非 npm)当您为了逃脱“回调地狱”却又误闯“Promise 地狱”您有很多来自 Promises 的.then 和.catchDatabase (数据库)下面是数据库驱动程序、ORM 和查询生成器的组合。在使用 ORM 之前,我强烈建议您首先确保需要使用它。当您可以只使用原始 SQL 或查询生成器时,它们通常会添加另一层抽象,这层抽象不一定能够提供足够的回报。mysql, node-postgres:当您不需要完整的 ORM,而是需要使用原始 SQL 查询数据库时使用(这些是驱动程序)node-mongodb-native:当您不需要一个完整的 ORM,而是要直接查询 MongoDB 时使用Mongoose:当您希望为 MongoDB 使用 ORM 时使用Knex:当您不需要一个完整的 ORM 解决方案,而只是需要一些工具使编写查询代码更容易,可以使用它Knex 是一个生成 SQL 的查询生成器您拥有 Postgres、MSSQL、MySQL、MariaDB、SQLite3、Oracle 或 Amazon Redshift 数据库Objection.js:您希望 ORM 支持 Knex 支持的所有东西,不使用查询 DSL(因此您编写的代码更接近原始 SQL),具有基于 Promise 的 API 和良好的文档Process management (进程管理)这个网址提供了部分进程管理器的横向比较http://strong-pm.io/compare/。注意:它们还包括了 StrongLoop Process Manager,这是一个不错的工具,但是有点笨重。我建议您在决定使用 StrongLoop 之前先查看一下解决方案。PM2:当您希望进程管理器在服务崩溃时处理重新启动,并允许您控制集群时使用注意:PM2 所依据的 AGPL 许可证存在一些潜在的违规行为。这里有一些讨论。我的看法是它最有可能被使用。但如果您有任何问题,请咨询您的法律部门,因为我不是律师。forever:当您需要进程管理器来处理在服务崩溃时重新启动服务时使用您的部署规模较小(pm2 及其集群支持用于更大规模的部署)。如果您只有少量的服务/进程,那么您可能可以使用它nodemon:当您希望监视应用程序中的任何代码更改时使用,并在本地开发时自动重启服务器非常适合用于开发!Web Sockets对于 Web Sockets,我只是推荐 primus,而不是列出一个列表。它支持所有主要的 Web Sockets 实现,并且维护者十分积极。如果您需要换成其他的库,您可以通过一行代码更改轻松地更换。Primus:当您需要 Web Sockets 但又不想被束缚在特定的 Web Sockets 实现时使用API documentation (API 文档)Swagger-node:当您需要记录 REST API 并能够针对端点测试请求时使用Utilities/misc (通用工具/杂项)Lodash:当您需要 JS 实用程序库时使用您使用了大量的 OOP(面向对象编程)Ramda:当您希望使用函数式的编程风格时,请使用您想要像 lodash 这样的东西,但是在函数式编程范式中Moment:在需要解析、验证、操作和显示日期/时间时使用UUID:当您需要随机的、唯一的、难以破解的 id 时使用NVM:当您希望能够在环境中安装的多个 Node 版本之间切换时使用Fs-extra:当您需要能够递归地使用mkdir、rm -rf和 Node 中缺少的其他文件系统级功能时,请使用Nodemailer:当您需要从 Node 发送电子邮件时使用Dotenv:当您需要将.env 文件中的环境变量加载到 process.env 时使用CLI (命令行界面)Commander:当您要构建一个 CLI 程序时使用,该程序将所有参数作为命令行上的标志Inquirer:当您想要构建一个按顺序获取选项的“交互式”CLI 程序时使用(类似于运行 npm init 时的方式,它会询问您生成 package.json 文件的一系列问题)Logging (日志)Winston:当您需要一个日志库并需要不同的日志输出格式时使用Bunyan:当您需要一个日志库,并以 JSON 作为唯一日志输出格式时使用您希望为不同的组件、请求或函数使用不同的日志记录器(也就是说,这些日志记录器可能以不同的方式解析事件)Morgan:当您使用 Express 并且想要记录 HTTP 请求时使用注意:这将与 Winston 或 Bunyan 一起使用。由于它是中间件,它知道如何处理请求并记录它,但不处理 Winston 和 Bunyan 所做的日志输出的传输。Templating (前端模板)Pug (原 Jade):当您需要服务器端模板引擎时,请使用该引擎,该引擎易于阅读,并且支持开箱即用的子组件代码块您只需要输出 HTMLEJS:当您需要一个服务器端模板引擎,该引擎完全使用 JS,并且允许空格缩进(Pug 不允许)注意:不支持异步 JS 函数Testing (测试)Mocha:在需要编写和运行单元测试时使用Chai:当您需要证明您的单元测试中的断言时,请使用注意:这将与 Mocha 一起使用Chai-as-promised:当您希望在 promises 上证明您的断言时,而不是将断言放在 then 或 catch 中使用Sinon:当您需要用于测试的 mock 库时使用Tooling (开发工具)ESdoc:当您想从您的代码中生成 API 文档,并且您正在使用最新的 JS 版本时,请使用默认情况下支持当前版本的 JS(支持 class),因此如果在代码中使用 prototypes,请使用 JSdocJSdoc:当您需要支持 ES6 的代码 API 文档生成器时使用支持classes 和 prototypesESlint:当您需要一个 linter 来自动查找(和修复)代码中的语法和代码格式问题时使用(译者注:可参考本人博文vscode + vetur + eslint + prettier 实现团队代码风格统一)Debugging (调试)现在,原生 Node 调试现在已经够用了,我的建议是直接使用它。几年前,引入一些 npm 模块是很有帮助的,而且您可能有一个特定的用例需要一个 npm 模块,但是现在已经有了足够的本地支持,如果您对调试没有任何太疯狂要求,请务必忽略掉额外的依赖项。结论挑选模块可能很难,但您只需要一些方法点来解决它。当您正在为如何抉择浪费时间,或者甚至不知道从哪里开始时,请使用本指南来帮助您。 ...

December 20, 2018 · 2 min · jiezi

解析Angularjs的$http异步删除数据及实例

这篇文章主要介绍了Angularjs的$http异步删除数据详解及实例的相关资料,这里提供实现思路及实现具体的方法,写的十分的全面细致,具有一定的参考价值,对此有需要的朋友可以参考学习下。如有不足之处,欢迎批评指正。Angularjs的$http异步删除数据详解及实例有人会说删除这东西有什么可讲的,写个删除的service,controller调用一下不就完了。嗯…看起来是这样,但是具体实现起来真的有这么简单吗?首先有以下几个坑怎么确定数据是否删除成功?怎么同步视图的数据库的内容?1.思路1.实现方式一删除数据库中对应的内容,然后将$scope中的对应的内容splice2.实现方式二删除数据库中对应的内容,然后再reload一下数据(也就是再调用一次查询方法,这种消耗可想而知,并且还要保证先删除数据再查询)2.具体实现方式删除数据的service:用异步,返回promiseservice(‘deleteBlogService’,//删除博客 [’$rootScope’, ‘$http’, ‘$q’, function ($rootScope, $http, $q) { var result = {}; result.operate = function (blogId) { var deferred = $q.defer(); $http({ headers: { ‘Content-Type’: ‘application/x-www-form-urlencoded;charset=UTF-8’ },//欢迎加入前端全栈开发交流圈一起学习交流:864305860 url: $rootScope.$baseUrl + “/admin/blog/deleteBlogById”, method: ‘GET’, dataType: ‘json’, params: { id: blogId } }) .success(function (data) { deferred.resolve(data); console.log(“删除成功!”); }) .error(function () { deferred.reject(); alert(“删除失败!”) }); return deferred.promise; }; return result; }])//欢迎加入前端全栈开发交流圈一起学习交流:864305860controller里面注意事项要特别注意执行顺序:确保己经删除完成之后再去reload数据,不然会出来视图不更新/** * 删除博客 */ $scope.deleteBlog = function (blogId) { var deletePromise = deleteBlogService.operate(blogId); deletePromise.then(function (data) { if (data.status == 200) { var promise = getBlogListService.operate($scope.currentPage); promise.then(function (data) { $scope.blogs = data.blogs; $scope.pageCount = $scope.blogs.totalPages; });//欢迎加入前端全栈开发交流圈一起吹水聊天学习交流:864305860 }//面向1-3年前端人员 });//帮助突破技术瓶颈,提升思维能力 };结语感谢您的观看,如有不足之处,欢迎批评指正。 ...

December 17, 2018 · 1 min · jiezi

细说后端模板渲染、客户端渲染、node 中间层、服务器端渲染(ssr)

细说后端模板渲染、客户端渲染、node 中间层、服务器端渲染(ssr)前端与后端渲染方式的发展大致经历了这样几个阶段:后端模板渲染、客户端渲染、node 中间层、服务器端渲染(ssr)。1. 后端模板渲染前端与后端最初的渲染方式是后端模板渲染,就是由后端使用模板引擎渲染好 html 后,返回给前端,前端再用 js 去操作 dom 或者渲染其他动态的部分。这个过程大致分成以下几个步骤:前端请求一个地址 url后端接收到这个请求,然后根据请求信息,从数据库或者其他地方获取相应的数据使用模板引擎(如 java > jsp、php > smarty)将这些数据渲染成 html将 html 文本返回给前端在这个过程中,前端的 html 代码需要嵌入到后端代码中(如 java、php),并且在很多情况下,前端源代码和后端源代码是在一个工程里的。所以,不难看出,这种方式的有这样的几个不足:前后端杂揉在一起,不方便本地开发、本地模拟调试,也不方便自动化测试前端被约束在后端开发的模式中,不能充分使用前端的构建生态,开发效率低下项目难以管理和维护,也可能会有前后端职责不清的问题尽管如此,但因为这种方式是最早出现的方式,并且这种渲染方式有一个好处,就是前端能够快速呈现服务器端渲染好的页面,而不用等客户端渲染,这能够提供很好的用户体验与 SEO 友好,所以当下很多比较早的网站或者需要快速响应的展示性网站仍然是使用这种方式。2. 客户端渲染随着前端工程化与前后端分离的发展,以及前端组件化技术的出现,如 react、vue 等,客户端渲染已经慢慢变成了主要的开发方式了。与后端模板渲染刚好相反,客户端渲染的页面渲染都是在客户端进行,后端不负责任何的渲染,只管数据交互。这个过程大致分成以下几个步骤:前端请求一个地址 url后端接收到这个请求,然后把相应的 html 文件直接返回给前端前端解析 js 后,然后通过 ajax 向后台获取相应的数据然后由 js 将这些数据渲染成页面这样一来,前端与后端将完全解耦,数据使用全 ajax 的方式进行交互,如此便可前后端分离了。其实,不难看出,客户端渲染与前后端分离有很大的好处:前端独立出来,可以充分使用前端生态的强大功能更好的管理代码,更有效率的开发、调试、测试前后端代码解耦之后,能更好的扩展、重构所以,客户端渲染与前后端分离现在已经是主流的开发方式了。但这种方式也有一些不足:首屏加载缓慢,因为要等 js 加载完毕后,才能进行渲染SEO 不友好,因为 html 中几乎没有可用的信息3. node 中间层为了解决客户端渲染的不足,便出现了 node 中间层的理念。传统的 B/S 架构中,是 浏览器 -> 后端服务器 -> 浏览器,上文所讲的都是这种架构。而加入了 node 中间层之后,就变成 浏览器 -> node -> 后端服务器 -> node -> 浏览器。这个过程大致分成以下几个步骤:前端请求一个地址 urlnode 层接收到这个请求,然后根据请求信息,向后端服务器发起请求,获取数据后端服务器接收到请求,然后根据请求信息,从数据库或者其他地方获取相应的数据,返回给 node 层node 层根据这些数据渲染好首屏 htmlnode 层将 html 文本返回给前端一个典型的 node 中间层应用就是后端提供数据、node 层渲染模板、前端动态渲染。这个过程中,node 层由前端开发人员掌控,页面中哪些页面在服务器上就渲染好,哪些页面在客户端渲染,由前端开发人员决定。这样做,达到了以下的目的:保留后端模板渲染、首屏快速响应、SEO 友好保留前端后分离、客户端渲染的功能(首屏服务器端渲染、其他客户端渲染)但这种方式也有一些不足:增加了一个中间层,应用性能有所降低增加了架构的复杂度、不稳定性,降低应用的安全性对开发人员要求高了很多4. 服务器端渲染(ssr)大部分情况下,服务器端渲染(ssr)与 node 中间层是同一个概念。服务器端渲染(ssr)一般特指,在上文讲到的 node 中间层基础上,加上前端组件化技术在服务器上的渲染,特别是 react 和 vue。react、vue、angular 等框架的出现,让前端组件化技术深入人心,但在一些需要首屏快速加载与 SEO 友好的页面就陷入了两难的境地了。因为前端组件化技术天生就是给客户端渲染用的,而在服务器端需要被渲染成 html 文本,这确实不是一件很容易的事,所以服务器端渲染(ssr)就是为了解决这个问题。好在社区一直在不断的探索中,让前端组件化能够在服务器端渲染,比如 next.js、nuxt.js、razzle、react-server、beidou 等。一般这些框架都会有一些目录结构、书写方式、组件集成、项目构建的要求,自定义属性可能不是很强。以 next.js 为例,整个应用中是没有 html 文件的,所有的响应 html 都是 node 动态渲染的,包括里面的元信息、css, js 路径等。渲染过程中,next.js 会根据路由,将首页所有的组件渲染成 html,余下的页面保留原生组件的格式,在客户端渲染。5. 另外不需要首屏快速加载、SEO 友好的,用全客户端渲染需要首屏快速加载、SEO 友好的,如果用了如 react、vue 等组件化技术,将不得不用 node 中间层与服务器端渲染如果技术团队不支持,不建议在需要首屏快速加载、SEO 友好的地方使用如 react、vue 等组件化技术前后端分离之后也可以做后端模板渲染,这样前端的调试可以搭配 handlebars、ejs 等模板引擎进行本地调试,而后端的调试则需要到测试机了后续更多博客,查看 https://github.com/senntyou/blogs作者:深予之 (@senntyou)版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证) ...

October 17, 2018 · 1 min · jiezi

vue配置开发,测试,生产环境api

前言:想实现通过不同的命令,打包调用不同环境的API,实现实现前端自动化部署。前端自动化部署工程比较复杂,这里只介绍通过不同的命令,打包的时候调用不同环境的API,例如:npm run build 调用开发环境接口,打包开发环境npm run build:test 调用测试环境接口,打包测试环境npm run build:prod 调用生产环境接口,打包生产环境vue项目用vue-cli脚手架安装完成之后,生成的项目中会有build,config这两个文件夹1、在build文件下新建webpack.test.conf.js在build文件下新建webpack.test.conf.js,将webpack.prod.conf.js内容复制过来。修改webpack.test.conf.js文件将const env = require(’../config/prod.env’);修改为:const env = require(’../config/test.env’);2、在config文件下新建test.env.js在config文件下新建test.env.js,将prod.env.js内容复制过来;分别在dev.env.js,test.env.js,prod.env.js中定义变量API_ROOT,dev.env.jstest.env.jsprod.env.js3、在build文件下新建test.js把build.js 内容复制到test.js将const webpackConfig = require(’./webpack.prod.conf’)修改为:const webpackConfig = require(’./webpack.test.conf’)4、修改package.json配置axios请求的时候,接口地址直接调用 process.env.API_ROOT 就好了,打包的时候执行 npm run build:test就是调用的测试接口地址

October 13, 2018 · 1 min · jiezi

如何构建大型的前端项目

如何构建大型的前端项目1. 搭建好项目的脚手架一般新开发一个项目时,我们会首先搭建好一个脚手架,然后才会开始写代码。一般脚手架都应当有以下的几个功能:自动化构建代码,比如打包、压缩、上传等功能本地开发与调试,并有热替换与热加载等功能本地接口数据模拟css 模块化检查并自动矫正不符合规范的代码,并优化代码格式社区已有很多模板(boilerplate)或工具帮助我们搭建一个现成的脚手架,比如:create-react-appvue-cliyeomanhtml5-boilerplatereact-boilerplatehackathon-starter当然,如果这些模板或工具都不能满足你的需求,你也可以搭建自己的脚手架,可以参考:搭建自己的前端脚手架怎样提升代码质量CSS 模块化css 的弱化与 js 的强化本地化接口模拟、前后端并行开发2. 定义好项目的目录结构对于大型目录而言,一个好的目录结构是非常必要的。一个好的目录结构应当具有以下的一些特点:解耦:代码尽量去耦合,这样代码逻辑清晰,也容易扩展分块:按照功能对代码进行分块、分组,并能快捷的添加分块、分组编辑器友好:需要更新功能时,可以很快的定位到相关文件,并且这些文件应该是很靠近的,而不至于到处找文件可以参考 目录结构优化3. 做好项目的组件化这里的组件化,并非框架层面的组件化,而是工程层面的组件化。当一个项目中的不同的区块需要共用同一段代码,或者不同项目之间需要共享同一个组件的时候,就需要做组件化了。组件化一般分为项目内的组件化和项目外的组件化。项目内的组件化是,同一个项目内不同区块共用的代码可以提取出来为一个单独的组件。项目外的组件化是,跨项目间共享的代码可以提取出来为一个单独的项目,然后如正常的 npm 包一样使用。可以参考:组件化私有 npm 仓库4. 封装团队的构建工具对于多项目而言,不管使用 webpack 还是 rollup 来打包项目,都会面临一些问题:不同项目有不同配置文件,更新配置需要到处改,而且很难保持一致如果有项目的升级,不同区块可能会有不同的配置这个时候,把配置文件封装成一个项目,然后使用版本化的方式管理,如正常的 npm 包一样使用,就会很方便了。比如,我的项目都是使用 lila 来构建的。5. 选好基础 js 框架、ui 框架、页面模板开始开发前,需要先选好基础 js 框架,比如 react、vue、angular、jquery 等,因为一旦选定,基本上后面都不能更换了,因为更换的成本太大了。选好基础 js 框架后,可以选一个比较好的 ui 框架,这样可以少写很多代码,可以参考 前端最受欢迎的 UI 框架。如果页面的定制化程度不高,可以选择一个比较好的页面模板,比如:AdminLTEvue-element-admingentelellangx-adminant-design-pro6. 测试一般前端测试分以下几种:单元测试:模块单元、函数单元、组件单元等的单元块的测试集成测试:接口依赖(ajax)、I/O 依赖、环境依赖(localStorage、IndexedDB)等的上下文的集成测试样式测试:对样式的测试E2E 测试:端到端测试,也就是在实际生产环境测试整个应用一般会用到下面的一些工具:jestenzymeseleniumpuppeteer另外,可以参考 聊聊前端开发的测试。7. 持续集成构建与测试一般大型工程的的构建与测试都会花很长的时间,在本地做这些事情的话就不太实际,这就需要做持续集成构建与测试了。持续集成工具用的比较多的:jenkinsgitlab cijenkins 是通用型的工具,可以与 github、bitbucket、gitlab 等代码托管服务配合使用,优点是功能强大、插件多、社区活跃,但缺点是配置复杂、使用难度较高。gitlab ci 是 gitlab 内部自带的持续集成功能,优点是使用简单、配置简单,但缺点是不及 jenkins 功能强大、绑定 gitlab 才能使用。另外,如果是开源项目,travis ci 是个不错的选择。后续更多博客,查看 https://github.com/senntyou/blogs作者:深予之 (@senntyou)版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证)

October 11, 2018 · 1 min · jiezi

Iceworks 支持小程序开发

目录引言Iceworks 为什么要支持小程序开发在 Iceworks 上如何开发小程序初始化项目启动服务新建页面项目基本结构开始你的第一个应用引言小程序开发无疑是目前前端领域炙手可热的开发方式之一,熟知的有支付宝小程序,微信小程序等;各种围绕着小程序的框架也开始在社区出现,如可以使用 Vue.js 开发小程序的 mpvue,遵循 React.js 语法规范的多端统一开发框架 Taro 等。这些框架的出现能有效的提升开发体验,一次编写,多端运行,开发者只需根据自己熟悉的语法,开发的小程序便可以运行在多个设备上,听起来很酷,确实也很酷。而 Iceworks 希望在这些很酷的基础上,能让前端开发更酷一点。Iceworks 为什么要支持小程序开发对于有小程序业务需求的团队而言,可能同时需要在多个小程序项目进行切换开发,在开发的过程中我们会发现有很多基础琐碎但又并不可少的一些工作:工程管理:之前见过一些团队是直接将旧的项目复制一份,删掉业务代码,只留下基础的项目结构和工程配置,然后在此基础上进行开发,这显然不是优雅的做法,太过于浪费时间,且没有形成团队的规范页面开发:这个过程可能是很多开发不愿意去做的事情,目前社区的小程序框架都会配备一套基础组件,这在一定程度上能减少 UI 开发的时间和效率提升,但还是少不了切页面的过程在飞冰中,我们基于不同的项目进行物料沉淀,可复用的代码块,不同行业的场景模板,让开发者直接在 Iceworks 上快速的从物料源中选择模板创建小程序应用,并可以在此基础上进行复用和修改,基于区块可视化的组装一个页面,从而提高开发效率,减少不必要的重复的工作,专注于业务开发。当然,除了飞冰提供的行业模板和区块,开发者也可以根据团队的开发规范和实际情况形成最佳实践,最后沉淀成区块物料池和脚手架模板。在 Iceworks 如何开发小程序升级到 Iceworks 2.9.0 版本,在设置面板开启 小程序物料源 选项,即可在模板界面和区块面板看到对应的小程序相关物料,其中包括:4 个模板miniapp-liteproducts-adminposts-adminoperating-dashboard18 个区块初始化项目在模板界面,可以看到 Iceworks 推荐的小程序物料源,这里我们选择第一个 Miniapp lite 模板进行项目初始化:启动服务初始化完成后,在 Iceworks 项目界面,点击启动调试服务,在浏览器打开对应的地址即可预览:新建页面接下来,使用 Iceworks 新建页面的功能来添加一个小程序页面,点击新建页面,根据需求选择对应的区块,可以在面板右侧看到效果图,可以按需删除或者新增,也可以点击预览页面功能看到实际的页面效果,最后可以生成页面,会下载对应的代码到初始化的项目脚手架中:回到浏览器,在地址栏输入新建页面的路由(例如: http://127.0.0.1:6002/#!/page2),即可看到实际的效果图。项目基本结构Iceworks 推荐的小程序物料源使用的是淘宝小程序轻框架语法,项目包含一个描述整体程序的入口和多个描述各自页面的页面级入口, 组件扩展名为 .html 的 Single File Component (单文件组件) 描述一个自定义的轻框架组件项目的主体部分由 manifest.json 和 app.js 组成,必须放在项目的根目录,如下:manifest.jsonapp.js- .iceworks/ // 模板文件(可自定义生成模板的格式)- public/ // 静态资源- src/ - components/ // 组件目录 - component1.html // 组件文件 - component2.html - pages/ // 页面目录 - page1/ - index.html // 页面入口 - page2/ - index.html - index/ - index.html- manifest.json // 描述项目基本信息,包括页面、tabBar等- app.js // 程序级应用入口- package.json // 项目工程文件具体开发文档参考:products-admin淘宝开发者平台 - 框架淘宝开发者平台 - 组件你的第一个轻框架应用在 Icewworks 中选择并创建应用后,跟着下文开始开发吧https://developer.taobao.com/…多端适配目前生成的应用是以 H5 的方式预览,同时支持 PWA 模式。 通过转换工具可以将应用发布到淘宝小程序,支付宝小程序,微信小程序等,实现多端统一,大大提高开发者的效率。扩展信息淘宝开发者平台官方网站:飞冰-让前端开发简单而友好下载 Iceworks:https://alibaba.github.io/ice…Github:http://github.com/alibaba/ice飞冰群二维码:点击这里查看二维码联系 & 招聘 ice-admin[at]alibaba-inc.com ...

September 24, 2018 · 1 min · jiezi

使用 ng-packagr 打包 Angular

写在前面为了让 Angular 类库应用范围更自由,Angular 提出一套打包格式建议名曰:Angular Package Format,包括 FESM2015、FESM5、UMD、ESM2015、ESM5、ES2015 格式,不同格式可以在不同的环境(Angular Cli、Webpack、SystemJS等)中使用。传统方式需要对这些格式逐一打包,一个示例打包脚本写法。这种写法只能针对不同项目的配置,而且除非你了解这些格式的本质否则很难维护;后来社区根据 APF 规范实现了类库 ng-packagr,通过简单的配置可以将你的类库打包成 APF 规范格式。至 V6 以后 Angular Cli 也基于 ng-packagr 实现了另一个 @angular-devkit/build-ng-packagr 应用构建器。如何使用既然 ng-packagr 被 Angular Cli 内置,这让我们进一步简化了生产一个 APF 规范格式的类库的成本。在 Angualr Cli 里使用 ng g library 来创建一个类库模板,例如在一个新的 Angular 应用里执行:ng g library <library name>而打包,则:ng build <library name>最终,将生成的 dist/<libary name> 目录下文件上传相应包管理服务器(例如:npm)提供给其他 人使用。配置说明由 Angular Cli 生成的类库模板大部分内容同 Angular 应用一样,只是多了一个 ng-package.json 的配置文件(对于生产环境是 ng-package.prod.json),它是专门针对 ng-packagr 的一个配置文件,如同 angular.json 一般也是基于 JSON Schema 格式,因此可以通过访问 ng-package.schema.json 了解所有细节,以下描述一些重点项。whitelistedNonPeerDependenciesng-packagr 默认会根据 package.json 的 peerDependencies 节点清单来决定类库所需要第三方依赖包,这些依赖包是不会被打包至类库。然而,所依赖包不存在 peerDependencies 节点里时(当然建议需要依赖的项应该在里面),就需要该属性的配置。lib/entryFile指定入口文件。lib/umdModuleIdsUMD 格式采用 rollup 打包,当类库需要引用一些无法猜出正确 UMD 标识符时,就需要你手动映射这些类库的标识。“umdModuleIds”: { “lodash”: “_"}angular.jsonAngular Cli 配置文件 angular.json 内会增加一个以 <libary name> 命名的构建配置,绝大多数配置性同普通 Angular 应用如出一辙,唯一不同的是 builder 节点为:“builder”: “@angular-devkit/build-ng-packagr:build"次级入口有时候一个类库可能会包含着多个二次入口,就像 @angular/core 类库包含着一个 @angular/core/testing 模块,它只是运用于测试,因此并不希望在项目中引入 @angular/core 时也包含测试代码,但同时二者又是同一个功能性时,这种次级导入显得非常重要。另一种像 ngx-bootstrap、@angular/cdk/ally 等都提供次级模块的导入,可以更好的优化体积。不论出于何种目的,都可以通过 Angular Cli 简单的文件组织进一步打包出主、次级分明的类库。ng g library 生成的结构大概如下:<libary name>├── src| ├── public_api.ts| └── lib/.ts├── ng-package.json├── ng-package.prod.json├── package.json├── tsconfig.lib.json└── tsconfig.spec.json当根目录下包含 README.md、LICENSE 时会自动被复制到 dist 目录中,Npm 规定必须包含 README.md 文件,否则访问已发布类库页时会有未找到描述文件错误提示。若想创建一个 <libary name>/testing 的次级入口,只需要在 <libary name> 根目录下创建一个 testing 目录:<libary name>├── src| ├── public_api.ts| └── lib/.ts├── ng-package.json├── ng-package.prod.json├── package.json├── tsconfig.lib.json├── tsconfig.spec.json└── testing ├── src | ├── public_api.ts | └── .ts └── package.json核心是需要提供一个 package.json 文件,而且内容简单到姥姥家。{ “ngPackage”: {}}最后,依然使用 ng build <libary name>,会产生一个次级导入模块。小结至此,基本上利用 Angular Cli 可以快速的构建一个可发布于 Npm Angular 类库,更复杂的可以构建像 ngx-bootstrap、@angular/cdk/ 类库。自定义构建Angular Cli 虽然提供非常便利的环境,但是对于一些复杂环境像 Delon 类库(ng-alain基建系列类库)包含着多个类库、类库又包含多个次级导入时,Angular Cli 会显得有点啰嗦,特别是对每个类库的 angular.json 配置。其实 @angular-devkit/build-ng-packagr 非常简单,如果将取进一步简化,整个实现差不多相当于:const path = require(‘path’);const ngPackage = require(’ng-packagr’);const target = path.resolve(__dirname, ‘./projects/<libary name>’);ngPackage .ngPackagr() .forProject(path.resolve(target, ng-package.prod.json)) .withTsConfig(path.resolve(target, ’tsconfig.lib.json’)) .build() .then(() => { // 构建完成后干点事 });将上面的代码放到 ./build.js,执行:node scripts/build.js其结果完成是等价。build() 返回的是一个 Promise 对象,意味着可以确保构建开始前和结束后做一点额外的事。总结ng-packagr 极大简化 Angular 类库被打包出一个 APF 规范建议,虽然它以 ng- 开头,但本质上并不一定非要在 Angular 中运用,也可以使用在 React、VUE。 ...

September 20, 2018 · 2 min · jiezi