webpack-htmlwebpackplugin

如果你的是用vue-cli生成你的vue项目,意味着生成的项目的默认webpack配置几乎不需要做什么修改,你通过npm run build就能得到可以用于发布的/dist文件夹,里面包含着一个index.html文件和build出来的output文件。如果打开/dist/index.html文件,大概你看到的是类似于这样: <!DOCTYPE html><html><head> <meta charset="UTF-8"> <title>Output Management</title></head><body><script type="text/javascript" src="index.65580a3a0e9208990d3e.js"></script><script type="text/javascript" src="main.3d6f45583498a05ab478.js"></script></body></html>这里值得注意的一点是,文件里面的index.65580a3a0e9208990d3e.js和main.3d6f45583498a05ab478.js,在每次执行npm run build之后这两个文件的文件名里面的hash值是可能变化的,而我们不可能每次都手动去修改这个index.html文件所引用的文件的名字吧?所幸,有这么一个plugin能帮我们做这件事,他就是:html-webpack-plugin。简单地来说我们需要html-webpack-plugin能做2件事: 生成用于发布的index.html文件自动替换每次build出来的output文件说了那么多也是废话,直接看代码来得直接:一:安装html-webpack-plugin npm install --save-dev html-webpack-plugin二:配置webpack.config.js const path = require('path');const HtmlWebpackPlugin = require('html-webpack-plugin');module.exports = { entry: { index: './src/index.js', main: './src/main.js' }, output: { path: path.resolve(__dirname, 'dist'), filename: '[name].[chunkhash].js', }, plugins: [ new HtmlWebpackPlugin({ }) ]}执行 npm run build得到:打开dist/index.html文件看下: <!DOCTYPE html><html><head> <meta charset="UTF-8"> <title>Webpack App</title></head><body><script type="text/javascript" src="index.65580a3a0e9208990d3e.js"></script><script type="text/javascript" src="main.3d6f45583498a05ab478.js"></script></body></html> 在我们的webpack.config.js文件里,我们只是new HtmlWebpackPlugin({}),没有给HtmlWebpackPlugin任何参数。可以看到HtmlWebpackPlugin做了2件事: 1: HtmlWebpackPlugin会默认生成index.html文件, 放到我们的dist/目录下面2:dist/index.html会自动更新我们每次build出来的文件在进行更多的探讨之前,我们有必要来先看看现目前项目的结构: 可以看到截止到目前我们的项目里面是没有任何html源文件的。 三:添加源文件index.html上一步呢我们只是new了一个没有任何参数的HtmlWebpackPlugin。其实HtmlWebpackPlugin有很多参数可以使用,下面我们介绍比较常用的几个。 1:我们先在项目的根目录下添加一个index.html源文件,里面的内容是: <!DOCTYPE html><html><head> <meta charset="UTF-8"> <title>index.html source file</title></head><body></body></html>2: 修改webpack.config.js,给HtmlWebpackPlugin添加参数: ...

May 7, 2019 · 1 min · jiezi

webpack中的webpackbaseconfigjs-和webpackconfigjs区别

npm run build是自定义添加的命令,package.json里面:scripts中应该有build命令的详细对应命令,某种意义上这算是一种alias,一种别名,npm run build也许是用的webpack另外加了一些参数,也有可能是用其他的打包命令 一直有点疑问webpack命名的问题,有很多的名字,也是醉了,就比如: webpack.base.config.jswebpack.config.js webpack.production.jswebpack.devserver.jswebpack.........查询了一下才知道:原来是随便命名的..........答案: 1.这些文件都是有什么不同 ,还是随意取名? webpack默认只认识webpack.config.js,只有这个是默认的,只有这个是默认的,默认的... 名字是可以随意取的,但是我们取名是为了让一些东西有意义不是?prod用于生产打包,dev用于开发打包,可以想象,prod和dev肯定有一部分配置相同,再加上一些不同的配置。所以相同的配置都放到base里面去,然后prod和dev再引入base,增加各自不同的细节。 2.webpack怎么识别? 看你的package.json,名字是可以随便取的,最终看你package.json调用哪个js,package.json的scripts里调用作者:HainesFreeman 来源:CSDN 原文:https://blog.csdn.net/weixin_... 版权声明:本文为博主原创文章,转载请附上博文链接!

May 5, 2019 · 1 min · jiezi

VueCLI2x全家桶架构支持打包后自动部署到服务器构建案例

今天有时间分享一些平时自己开发上的一些构建配置,我们以Vue-CLI-2.x来构建开发环境。好,我们先来看一下我们要做哪些工作。现附上源码地址,https://github.com/749264345/... 1.Vue,Vuex,vue-router,axios通过CDN引入;优化打包后文件过大的问题2.开发环境区分开发,测试,生产;提高开发效率3.打包区分测试与生产4.实现项目打包后自动部署到远程服务器5.模块化构建vuex状态管理6.打包后自动清除console.log()7.项目默认基于Element-ui,对相关组件二次封装8.模块化构建axios请求响应拦截器一寸光阴一寸金,寸金难买寸光阴~废话不多说,直接撸起袖子就是干~~ 1.Vue,Vuex,vue-router,axios通过CDN引入在过去我们都习惯使用npm或者cnpm安装依赖,因为这种操作非常方便,但是有利必有弊,有些小伙伴在将项目打包后会发现打包后的js文件非常庞大,有些甚至达到了几兆,所以最终导致应用打开时首屏加载非常慢,因此我们可以使用传统的引入方式,也就是CDN引入。Vue,Vuex,vue-router,axios,这些均为静态资源,我们从它们官网下载适合的版本后放入项目根目录【static】文件夹,然后修改几个地方:首先,index.html <!--兼容ie--> <script src="./static/libs/polyfill.min.js"></script> <script src="./static/libs/eventsource.js"></script> <!--vue--> <script src="./static/libs/vue.js"></script> <script src="./static/libs/vue-router.min.js"></script> <script src="./static/libs/element-ui/element-ui.js"></script> <script src="./static/libs/axios.js"></script> <script src="./static/libs/vuex.js"></script>然后修改,build/webpack.base.conf.js module.exports = { externals: { 'vue': 'Vue', 'vuex': 'Vuex', 'vue-router': 'VueRouter', 'axios': 'axios' }, ...}到这里基本配置已经完成,最后一步需要在main.js中删除原先对这些模块的import操作即可。这样后再打包项目,你会发现页面非常丝滑,几乎秒开。 2.开发环境区分开发,测试,生产;提高开发效率在调试接口,或者debug的时候我们经常会切换环境,而且在打包的时候又要改成生产的接口,这样没有效率,所以我们可以做如下配置。在config/dev.env.js文件中, const TARGET = process.env.npm_lifecycle_event;//开发环境if (TARGET === 'dev') { var data = { NODE_ENV: '"dev"', API: '"http://www.dev.com"' }}//测试环境if (TARGET === 'test') { var data = { NODE_ENV: '"test"', API: '"http://www.test.com"' }}//生产环境if (TARGET === 'prod') { var data = { NODE_ENV: '"prod"', API: '"http://www.prod.com"' }}我们从process.env.npm_lifecycle_event中获得当前node执行的环境来区分项目运行的环境,这样我们可以用来区分接口环境,因此我们添加相关指令。在根目录package.json文件中, ...

May 5, 2019 · 3 min · jiezi

关于vue项目打包后背景图路径找不到问题

1、情况一:使用MiniCssExtractPlugin插件 MiniCssExtractPlugin打包后背景图片路径404问题,解决方法: 2、情况二:使用ExtractTextPlugin插件 ExtractTextPlugin打包后背景图片路径404问题,解决方法:

May 5, 2019 · 1 min · jiezi

谈谈前端工程化-js加载

当年的 js 加载在没有 前端工程化之前,基本上是我们是代码一把梭,把所需要的库和自己的代码堆砌在一起,然后自上往下的引用就可以了。 那个时代我们没有公用的cdn,也没有什么特别好的方法来优化加载js的速度。最多用以下几个方案。 可用的性能方案可以在代码某些需要js的时候去使用 loadjs 来动态加载 js 库。这样就不会出现开始时候加载大量js文件。再大点的项目可能用一下 Nginx ngx_http_concat_module 模块来合并多个文件在一个响应报文中。也就是再加载多个小型 js 文件时候合并为一个 js 文件。BigPipe 技术也是可以对页面分块来进行优化的,但是因为与本文关系不大,方案也没有通用化和规范化,加上本人其实没有深入了解所不进行深入介绍,如果先了解可以参考 新版卖家中心 Bigpipe 实践(一) 以及 新版卖家中心 Bigpipe 实践(二)。当然那个时期的代码也没有像现在的前端的代码量和复杂度那么高。 Webpack 之后的js加载与其说 Webpack 是一个模块打包器,倒不如说 Webpack 是一份前端规范。 需要库没有被大量使用情况对于我们代码中所需要的代码库没有大量使用,比如说某种组件库我们仅仅只使用了 2、3个组件的情况下。我们更多需要按需加载功能。 比方说在 MATERIAL-UI 我们可以用 import TextField from '@material-ui/core/TextField';import Popper from '@material-ui/core/Popper';import Paper from '@material-ui/core/Paper';import MenuItem from '@material-ui/core/MenuItem';import Chip from '@material-ui/core/Chip';代替 import { TextField, Popper, Paper, MenuItem, Chip} from '@material-ui'这样就实现了按需加载,而不是动辄需要整个组件库。但是这样的代码中这样代码并不好书写。我们就需要一个帮助我们转换代码的库。这可以参考 Babel 插件手册 以及 简单实现项目代码按需加载 来实现我们的需求。 ...

May 3, 2019 · 1 min · jiezi

webpack-4迁移指南

webpack 4 出来也已经一年了,公司的老项目用的还是webpack3,也是时候该升级一波了。说实话webpack4还是有几点挺吸引我的,估计也是感受到了parcel的压力,4这个版本内置了很多默认配置。迁移的坑CommonsChunkPlugin 废除, webpack 4内置了optimization.splitChunks 和 runtimeChunk 进行代码拆分ExtractTextPlugin 不支持webpack4,官方推荐了一个MiniCssExtractPlugin来拆分chunk中的css代码vue-loader 的升级,vue-loader 版本由14 升级到了15, 之前的配置也改变了具体参考从 v14 迁移基础配置const package = require('../package.json');const path = require('path');const VueLoaderPlugin = require('vue-loader/lib/plugin');const utils = require('./utils');function resolve (dir) { return path.join(__dirname, dir);}module.exports = { entry: { main: '@/main', 'vender-exten': '@/vendors/vendors.exten.js' }, output: { path: path.resolve(__dirname, '../dist/' + package.version) // 输出文件的绝对路径 }, module: { rules: [ { test: /\.(js|vue)$/, loader: 'eslint-loader', enforce: 'pre', exclude: /node_modules/, options: { emitError: true, emitWarning: false, failOnError: true } }, { test: /\.vue$/, exclude: /node_modules/, loader: 'vue-loader' }, { test: /\.js$/, exclude: /node_modules/, use: { loader: 'babel-loader' } }, { test: /\.js[x]?$/, exclude: /node_modules/, use: { loader: 'babel-loader' } }, { test: /\.(png|jpe?g|gif|svg)(\?.*)?$/, use: { loader: 'url-loader', options: { limit: 10000, name: utils.assetsPath('img/[name].[hash:7].[ext]') } } }, { test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/, use: { loader: 'url-loader', options: { limit: 10000, name: utils.assetsPath('media/[name].[hash:7].[ext]') } } }, { test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/, use: { loader: 'url-loader', options: { limit: 10000, name: utils.assetsPath('fonts/[name].[hash:7].[ext]') } } } ] }, plugins: [ new VueLoaderPlugin(), ], resolve: { extensions: ['.js', '.vue'], alias: { '@': resolve('../src') } }, externals: { AMap: 'AMap', vue: 'Vue', iview: 'iview', 'vue-router': 'VueRouter', vuex: 'Vuex' }};如上,实现了对vue,jsx, js, 和资源文件对处理和转译 ...

May 3, 2019 · 3 min · jiezi

从零开始开发一个react脚手架五

前言:最近天天加班做新项目,Taro版的小程序,还要实现富文本加海报,踩了不少坑,下次专门开个坑说一下。 回到脚手架,说实话从头写一个,即便是参考create-react-app,还是遇到了很多很多问题,这些都是需要自己亲自写过,才能有所体会。这次会写的有杂乱一点,就说说自己遇到的哪些问题。虽然还没有全部完工,但是已经能够基本使用,最下面会贴git地址 问题一 现在基本实现了两个命令,start和build,start就是启动webpack-dev-server,这是开发环境, build就是构建,是production环境。 不管什么命令都需要依赖 NODE_ENV=development或者production,这样会导致每次npm run 的时候都要加上NODE_ENV,而且这个还不能直接写在script命令里面,因为window是不支持,需要安装第三方库。最后参考create-react-app 直接赋值,简单粗暴。 一开始没想到可以这么简单粗暴,走了歪路,想要通过DefinePlugin来实现赋值,但其实这反倒是错误的路线。需要理解编译工具所依赖的NODE_ENV和业务代码里面的NODE_ENV是不同的,DefinePlugin其实就是个简单的全局变量替换,只能替换chunk里面的。 问题二 优化createWebpack,一开始的做法极为简单,直接export一个对象,导致灵活性很差,后来改成导出一个方法,接收各种配置参数,返回一个webpackConfig。 问题三 webpack-dev-serve,这个问题捣鼓了很久很久。关于webpack-dev-serve的contentBase配置。一开始我设置的是dist目录,也就是打包后的目录,即output设置的path。因为以前都是这样玩的,但是这样意味着服务依赖了一个打包构建后的目录dist,否则没有办法找到index.html。这样就很诡异,难道第一次start都必须先build一下吗?很不合理,而且build的时候NODE_ENV必须是development,否则js的引用地址就成了线上地址。而根据上面一个问题,简单粗暴在代码里面写死NODE_ENV就很难改变环境变量了,build出来的只能是线上。看了很久的create-react-app的源码,因为这个脚手架并不需要先build,一开始以为他在start的时候做了什么特殊处理,后来才发现压根不是,它的webpack-dev-server配置的cotentbase竟然不是dist,而是原始目录src/index.html。。。这样我很震惊啊,因为我想的是,js什么的,css什么的引入都必须是在webpack build之后才知道的,因为会做拆分剥离的处理,JS的文件个数和名字不确定,如果引入的是原始目录,那么到底是在哪儿做的JS和CSS插入操作呢,为此寻遍了源码,一个个去找plugin,但最后发现其实压根很简单。在于html-webpack-plugin,貌似它会先在内存中生成一份html,想要访问内存地址直接就是localhost:3000,而webpack-dev-server应该是优先去取内存中的index.html,没取到就会去cotentbase中找,也就是说和cotentbase压根没有半毛钱关系。产生错觉的原因在于,内存中没找到,而cotentbase中同样没找到,就会报404。至于为嘛没有取到内存中的值,在于我闲的没事,设置了publicPath,为/assets/这样导致,想要访问html的地址就变成了localhost:3000/assets/index.html。其实开发环境压根没必要设置assets,没半毛钱作用。 问题四 这个就是很蠢的一个问题了,很粗心。关于dependencies和devDependencies,其实单纯问两者的区别,大家都知道,一个是开发依赖,一个是线上依赖。但是我们实际开发中,不管是你开发依赖还是线上依赖,其实所有的包都会安装,所以有的时候装错地方,也没啥的。but。。。如果你开发的是第三方包,这个问题就大了。。。如果是第三方包,只会安装第三方包的dependencies。因为一开始的开发不规范,导致我随意乱装。。结果查了半天,一直报XXX包找不到。。捣鼓了很久,才发现是个这么简单的问题。 问题五 关于npm publishi的时候如果报权限或者什么其他问题的错,十有八九是 设定了源的路径,如果是淘宝源或者公司源,需要切换回npm默认源,才可以发布。 问题暂时就想到这么多了。其实脚手架最简单的反而是webpack配置了,这些东西一搜索网上一大把,配置消息我就不多说了。说几个值得注意的地方。。第一就是splitChunks,这个设置拆包大小的时候记得注意单位,30000这个是30kb。所以我写的是100000,意味着要拆成独立的包,压缩前必须有100KB,其实还可以设置大一点,你想想100KB压缩后估计就50,再来个GZIP就是20KB左右了。一个包可以再大点。但如果是http2,也可以不用拆太大,毕竟多路复用吗。 第二就是关于静态资源引用的问题,其实我觉得本地业务代码里面完全不用引入静态资源,通通丢到云服务器就好,比如静态图片,我都是通通扔到七牛服务器,代码里面直接写死,爽歪歪,还能享受CDN加成和http2. react+router+redux 说的有点乱,想到啥写啥。主要是事件间隔有点久了,最近加班太忙。 脚手架已经实现了三分之一,现在是直接clone git来作为脚手架,到最后效果应该是npx的形式,不过命令内容已经实现 easy-react start和 easy-react build。代码已经更新到git了,方便给个star。 easy-react即命令的实现源码还没扔上去,等后续完工了再扔上去。毕竟还有服务器端的部分没实现。 脚手架工具很多,我觉得有时间还是自己维护一份的好,想咋玩就咋玩,想升级就升级,自己定制。 git initgit clone https://github.com/417673259/...npm install npm run start 有问题就留言,必定回复,给个赞呗

May 3, 2019 · 1 min · jiezi

vueloader-详解

vue-loader.conf.js 详解//此文件是处理.vue文件的配置文件'use strict'//导入utils.js工具const utils = require('./utils')//导入config文件夹下的index.jsconst config = require('../config')//判断是不是生产环境const isProduction = process.env.NODE_ENV === 'production'//根据环境来获取相应的productionSourceMap或者cssSourceMapconst sourceMapEnabled = isProduction?config.build.productionSourceMap:config.dev.cssSourceMap //导出module.exports = { loaders:utils.cssLoaders({ //是否开始sourceMap 用来调试 sourceMap:sourceMapEnabled, //是否单独提取抽离css extract:isProduction }), //记录压缩的代码,用来找到源码位置 cssSourceMap:sourceMapEnabled, //是否缓存破坏 cacheBusting:config.dev.cacheBusting, //transformToRequire的作用是在模块编译的过程中,编译器可以将某些属性,比如src转换为require调用 transformToRequire:{ video:['src','poster'], source:'src', img:'src', image:'xlink:href' }}原文:https://blog.csdn.net/xiaoxia...

April 29, 2019 · 1 min · jiezi

webpack-Code-Splitting浅析

Code Splitting是webpack的一个重要特性,他允许你将代码打包生成多个bundle。对多页应用来说,它是必须的,因为必须要配置多个入口生成多个bundle;对于单页应用来说,如果只打包成一个bundle可能体积很大,导致无法利用浏览器并行下载的能力,且白屏时间长,也会导致下载很多可能用不到的代码,每次上线用户都得下载全部代码,Code Splitting能够将代码分割,实现按需加载或并行加载多个bundle,可利用并发下载能力,减少首次访问白屏时间,可以只上线必要的文件。三种Code Splitting方式webpack提供了三种方式来切割代码,分别是: 多entry方式公共提取动态加载本文将简单介绍多entry方式和公共提取方式,重点介绍的是动态加载。这几种方式可以根据需要组合起来使用。这里是官方文档,中文 英文 多entry方式这种方式就是指定多个打包入口,从入口开始将所有依赖打包进一个bundle,每个入口打包成一个bundle。此方式特别适合多页应用,我们可以每个页面指定一个入口,从而每个页面生成一个js。此方式的核心配置代码如下: const path = require('path');module.exports = { mode: 'development', entry: { page1: './src/page1.js', page2: './src/page2.js' }, output: { filename: '[name].bundle.js', path: path.resolve(__dirname, 'dist') }};上边的配置最终将生成两个bundle, 即page1.bundle.js和page2.bundle.js。 公共提取这种方式将公共模块提取出来生成一个bundle,公共模块意味着有可能有很多地方使用,可能导致每个生成的bundle都包含公共模块打包生成的代码,造成浪费,将公共模块提取出来单独生成一个bundle可有效解决这个问题。这里贴一个官方文档给出的配置示例: const path = require('path'); module.exports = { mode: 'development', entry: { index: './src/index.js', another: './src/another-module.js' }, output: { filename: '[name].bundle.js', path: path.resolve(__dirname, 'dist') }, // 关键 optimization: { splitChunks: { chunks: 'all' } } };这个示例中index.js和another-module.js中都import了loadsh,如果不配置optimization,将生成两个bundle, 两个bundle都包含loadsh的代码。配置optimization后,loadsh代码被单独提取到一个vendors~another~index.bundle.js。 ...

April 29, 2019 · 3 min · jiezi

手写一个webpack插件

本文示例源代码请戳github博客,建议大家动手敲敲代码。webpack本质上是一种事件流的机制,它的工作流程就是将各个插件串联起来,而实现这一切的核心就是Tapable,webpack中最核心的负责编译的Compiler和负责创建bundles的Compilation都是Tapable的实例。Tapable暴露出挂载plugin的方法,使我们能 将plugin控制在webapack事件流上运行(如下图)。 Tabable是什么?tapable库暴露了很多Hook(钩子)类,为插件提供挂载的钩子。 const { SyncHook, SyncBailHook, SyncWaterfallHook, SyncLoopHook, AsyncParallelHook, AsyncParallelBailHook, AsyncSeriesHook, AsyncSeriesBailHook, AsyncSeriesWaterfallHook } = require("tapable"); Tabable 用法 1.new Hook 新建钩子 tapable 暴露出来的都是类方法,new 一个类方法获得我们需要的钩子。class 接受数组参数options,非必传。类方法会根据传参,接受同样数量的参数。const hook1 = new SyncHook(["arg1", "arg2", "arg3"]);2.使用 tap/tapAsync/tapPromise 绑定钩子tapable提供了同步&异步绑定钩子的方法,并且他们都有绑定事件和执行事件对应的方法。 -Async*Sync*绑定tapAsync/tapPromise/taptap执行callAsync/promisecall3.call/callAsync 执行绑定事件 const hook1 = new SyncHook(["arg1", "arg2", "arg3"]);//绑定事件到webapck事件流hook1.tap('hook1', (arg1, arg2, arg3) => console.log(arg1, arg2, arg3)) //1,2,3//执行绑定的事件hook1.call(1,2,3)举个例子 定义一个Car方法,在内部hooks上新建钩子。分别是同步钩子 accelerate(accelerate接受一个参数)、break、异步钩子calculateRoutes使用钩子对应的绑定和执行方法calculateRoutes使用tapPromise可以返回一个promise对象。//引入tapableconst { SyncHook, AsyncParallelHook } = require('tapable');//创建类class Car { constructor() { this.hooks = { accelerate: new SyncHook(["newSpeed"]), break: new SyncHook(), calculateRoutes: new AsyncParallelHook(["source", "target", "routesList"]) }; }}const myCar = new Car();//绑定同步钩子myCar.hooks.break.tap("WarningLampPlugin", () => console.log('WarningLampPlugin'));//绑定同步钩子 并传参myCar.hooks.accelerate.tap("LoggerPlugin", newSpeed => console.log(`Accelerating to ${newSpeed}`));//绑定一个异步Promise钩子myCar.hooks.calculateRoutes.tapPromise("calculateRoutes tapPromise", (source, target, routesList, callback) => { // return a promise return new Promise((resolve,reject)=>{ setTimeout(()=>{ console.log(`tapPromise to ${source} ${target} ${routesList}`) resolve(); },1000) })});//执行同步钩子myCar.hooks.break.call();myCar.hooks.accelerate.call('hello');console.time('cost');//执行异步钩子myCar.hooks.calculateRoutes.promise('i', 'love', 'tapable').then(() => { console.timeEnd('cost');}, err => { console.error(err); console.timeEnd('cost');})运行结果 ...

April 28, 2019 · 4 min · jiezi

你对项目里的依赖包了解吗

注意:本文所有依赖包是目前最新版本的 现在很多开发朋友对于使用webapck、babel搭建开发环境已经不陌生,但很少去系统性的了解项目依赖。 本文从环境依赖包说起,让你对自己的开发环境有更深的了解。 为了简单,我们将依赖分个类:Babel相关????、Webpack相关????、可选的依赖包。注意:带???? 是指必需的依赖, 下面我们一个一个来说。 Babel相关????要使用最新的ES6+语法,必须少不了Babel转码,那么要搭建一个完全体的环境,应该使用哪些依赖呢? 首先,我们安装最核心的依赖: @babel/cli、@babel/core、@babel/polyfill、@babel/register、core-js 下面是他们的一些简单解释: { /* Babel 自带了一个内置的 CLI 命令行工具,可通过命令行编译文件。 */ "@babel/cli": "^7.4.3", /* 看到`core`就知道它是`babel`的核心,一些转码操作都是基于它完成的, 所以它是必须的依赖。 */ "@babel/core": "^7.4.3", /* Babel默认只转换新的JavaScript语法,但是不转换新的API,比如 `Iterator`、`Generator`、`Set`、`Maps`、`Proxy`、`Reflect`、 `Symbol`、`Promise` 等全局对象,以及一些定义在全局对象上的方法(比 如 `Object.assign` )都不会转码。而`@babel/polyfill`就可以做到。 */ "@babel/polyfill": "^7.4.3", /* 让webpack.config.babel.js也支持ES6语法 */ "@babel/register": "^7.4.0", /* 通俗说就是动态polyfill,它可以动态加载需要的新API,具体可以看https://github.com/zloirock/core-js#readme */ "core-js": "3", }下面我们安装必需的preset和plugin:@babel/preset-env、@babel/plugin-proposal-class-properties、@babel/plugin-proposal-decorators、@babel/plugin-proposal-object-rest-spread、@babel/plugin-syntax-dynamic-import 下面是它们的一些解释: { /* 根据指定环境来转码,这个不用说,必装 */ "@babel/preset-env": "^7.4.3", /* 对class中属性初始化语法、static等语法进行处理 */ "@babel/plugin-proposal-class-properties": "^7.4.0", /* 装饰器语法处理 */ "@babel/plugin-proposal-decorators": "^7.4.0", /* 对象rest、spread语法处理 */ "@babel/plugin-proposal-object-rest-spread": "^7.4.3", /* import()语法处理 */ "@babel/plugin-syntax-dynamic-import": "^7.2.0",}安装好了以上preset和plugins,我们需要新建一个.babelrc文件来使用它们: ...

April 27, 2019 · 2 min · jiezi

详解webpack-urlloader和fileloader

大家平时使用url-loader和file-loader的时候有没有经常遇到以下这些问题或者疑问: 开发环境的时候图片路径好好的,怎么发布到线上就404了???或者说html里面引用的img路径是正确的,怎么到css里面路径404了?图片路径到底是怎么拼接???这两个到底是什么关系啊???怎么less里面引用的背景图片路径/import其他的less文件路径不对???如果恰好你也有以上这些问题或者疑问,那正好这篇文章能给你很好的解答注:这两个loader不仅可以处理图片,还可以处理音频,视频,字体等文件 url-loader作用如果页面图片较多,发很多http请求,会降低页面性能。这个问题可以通过url-loader解决。url-loader会将引入的图片编码,生成dataURl并将其打包到文件中,最终只需要引入这个dataURL就能访问图片了。当然,如果图片较大,编码会消耗性能。因此url-loader提供了一个limit参数,小于limit字节的文件会被转为DataURl,大于limit的还会使用file-loader进行copy file-loader作用在css文件中定义background的属性或者在html中引入image的src,我们知道在webpack打包后这些图片会打包至定义好的一个文件夹下,和开发时候的相对路径会不一样,这就会导致导入图片路径的错误。而file-loader正是为了解决此类问题而产生的,他修改打包后图片的储存路径,再根据配置修改我们引用的路径,使之对应引入 联系url-loader内部封装了file-loader。url-loader不依赖于file-loader,即使用url-loader时,只需要安装url-loader即可,不需要安装file-loader。通过上面的介绍,我们可以看到,url-loader工作分两种情况:1.文件大小小于limit参数,url-loader将会把文件转为DataURL;2.文件大小大于limit,url-loader会调用file-loader进行处理,参数也会直接传给file-loader。因此我们只需要安装url-loader即可 基本用法由于url-loader包含了file-loader所以,file-loader内的option在url-loader中均能使用如下为file-loader内的属性 如下为url-loader内的属性 接下来摘取几个重要的属性做说明 outputPath该属性指明我们最终导出的文件路径 最终导出的文件路径 === output.path + url-loader.outputPath + url-loader.name publicPath(常用于生成环境)该属性指明我们最终引用的文件路径(打包生成的index.html文件里面引用资源的前缀) 最终引用的文件路径前缀 === output.publicPath + url-loader.publicPath + url-loader.name name该属性指明文件的最终名称。 同样的,我们可以直接把outputPath的内容写到name中,一样可以生成对应的路径 经过上面的说明,我们得出结论,最终的静态文件路径(图片,音频,视频,字体等)=== output.publicPath + url-loader.publicPath + output.path + url-loader.outputPath + url-loader.name 有了上述的基础,我们通过实例来说明下开篇提出的4个问题 实例说明 打包后的文件结构 img里面的四个绿色的文件除去home-logo.png都是大于10kb的大图片,其他都是小于10kb的小图标 以上两个截图分别是开发环境和生成环境的图片引用路径 开发环境的时候图片路径好好的,怎么发布到线上就404了???或者说html里面引用的img路径是正确的,怎么到css里面路径404了? 答:其实大家仔细想一想就能知道答案,我们在本地开发的时候都是localhost:8080/下面的根目录,所以当图片生成如下的绝对地址是不会出问题的,可是你把同样的webpack配置放到生成环境上就不一定了,因为生成环境大部分的前端静态文件都不是在根目录啊,有可能就是这样的目录结构 www/ +folder/ +static/ +css/ +img/ +js/ +index.html这样你开发环境的绝对路径放到服务器上面自然就404了,所以要不然用相对路径,要不然就统一写死绝对路径 (同样道理,解释为什么css里面的背景图路径404,但是这个解决起来需要用到extract-text-webpack-plugin或者mini-css-extract-plugin这个插件,前者用于webpack4以下版本,后者是4以上版本,配置options里面的publicPath) 图片路径到底是怎么拼接??? 答:output.publicPath + url-loader.publicPath + output.path + url-loader.outputPath + url-loader.name这两个到底是什么关系啊??? 答:上面基本上都说过了,总结一句话就是相互互补的关系,url-loader不能转base64的时候file-loader来处理它怎么less里面引用的背景图片路径/import其他的less文件路径不对??? 答:这里面的涉及的东西有点多,我打算再开一篇文章来说了文章到这里就结束了,希望能帮助到大家(BTW,语言组织的依旧很差) ...

April 25, 2019 · 1 min · jiezi

webpack4入门学习笔记四Tree-Shaking与拆分配置文件

系列博客链接 webpack4入门学习笔记(一)webpack4入门学习笔记(二)webpack4入门学习笔记(三)--Babel的使用webpack4入门学习笔记(四)--Tree Shaking与拆分配置文件笔记的代码是在前面笔记基础上修改的,可以下载代码:github参考或是先看前面的笔记。 Tree Shaking使用Tree Shaking可以用来剔除JavaScript中用不上的死代码。它依赖静态的ES6模块化语法,例如通过import和export导入导出。 需要注意的是要让Tree Shaking正常工作的前提是JavaScript代码必须采用ES6模块化语法,因为ES6模块化语法是静态的,这让Webpack可以简单的分析出哪些export的被import过了。 接下来配置Webpack让Tree Shaking生效 webpack4默认保留ES6模块化语句,并没有通过Babel将其转换修改.babelrc文件为如下: //.babelrc{ "presets": [["@babel/preset-env",{ "useBuiltIns": "usage", "corejs": 2, "modules":false //关闭 Babel 的模块转换功能,保留原本的 ES6 模块化语法 //默认是auto,取值还可以是 amd, umd, systemjs, commonjs,auto等 }]]}修改webapck.config.js,添加 optimization: { usedExports: true}到module.exports{}中 module.exports={ mode: 'development', optimization: { //开发坏境使用tree shaking时加usedExports: true usedExports: true },}还需通过package.json的"sideEffects"属性来告诉webpack哪些模块是可以忽略掉,如果没有则设置为false,来告知webpack,它可以安全地删除未用到的export。 修改package.json { "name": "your-project", "sideEffects": false}有的模块没有导出模块,在Tree Shaking模式下就会被忽略,所以我们需要把这些模块做处理,不需要Tree Shaking对这些模块进行处理,可以改为一个数组: { "name": "your-project", "sideEffects": ["*.css"]}"sideEffects": ["*.css"]表示不对所以css模块使用Tree Shaking处理。 index.js //tree shaking import exportimport {cube} from './math.js'let component = () => { let element = document.createElement('pre') element.innerHTML = [ 'Hello webpack!', '2 cubed is equal to ' + cube(2) ].join('\n\n'); console.log(cube) return element;}document.body.appendChild(component());main.js ...

April 25, 2019 · 2 min · jiezi

手把手教你写一个-Webpack-Loader

本文不会介绍loader的一些使用方法,不熟悉的同学请自行查看Webpack loader1、背景首先我们来看一下为什么需要loader,以及他能干什么?webpack 只能理解 JavaScript 和 JSON 文件。loader 让 webpack 能够去处理其他类型的文件,并将它们转换为有效模块,以供应用程序使用,以及被添加到依赖图中。 本质上来说,loader 就是一个 node 模块,这很符合 webpack 中「万物皆模块」的思路。既然是 node 模块,那就一定会导出点什么。在 webpack 的定义中,loader 导出一个函数,loader 会在转换源模块resource的时候调用该函数。在这个函数内部,我们可以通过传入 this 上下文给 Loader API 来使用它们。最终装换成可以直接引用的模块。 2、xml-Loader 实现前面我们已经知道,由于 Webpack 是运行在 Node.js 之上的,一个 Loader 其实就是一个 Node.js 模块,这个模块需要导出一个函数。 这个导出的函数的工作就是获得处理前的原内容,对原内容执行处理后,返回处理后的内容。一个简单的loader源码如下 module.exports = function(source) { // source 为 compiler 传递给 Loader 的一个文件的原内容 // 该函数需要返回处理后的内容,这里简单起见,直接把原内容返回了,相当于该 Loader 没有做任何转换 return source;};由于 Loader 运行在 Node.js 中,你可以调用任何 Node.js 自带的 API,或者安装第三方模块进行调用: const xml2js = require('xml2js');const parser = new xml2js.Parser();module.exports = function(source) { this.cacheable && this.cacheable(); const self = this; parser.parseString(source, function (err, result) { self.callback(err, !err && "module.exports = " + JSON.stringify(result)); });};这里我们事简单实现一个xml-loader; ...

April 25, 2019 · 3 min · jiezi

web前端性能优化

前言性能优化是每个项目都需要注意的问题,在这里结合项目实际情况较为系统的整理下 正文web前端性能优化简单可以概括为两点 1、减少需要加载资源的体积1、按需加载就是每个页面之加载需要的内容,这一点在多页应用中一般都有注意,单页应用可以用webpack的import与vue的异步组件实现;react由于本人不熟,肯定也有相应的解决方案 2、资源压缩在目前的webpack4+中,只要将mode设置为production,webpack就会将对应的资源(html、css、js)进行压缩;gulp也有对应的模块(html-min,uglify) 标题文字2)加快加载的速度

April 24, 2019 · 1 min · jiezi

webpack4入门学习笔记三Babel的使用

系列博客链接 webpack4入门学习笔记(一)webpack4入门学习笔记(二)webpack4入门学习笔记(三)--Babel的使用代码 下载代码(demo3):github 本文的代码是在前面笔记基础上修改的,可以下载代码:githubl参考或是先看前面的笔记。 Babel是一个广泛使用的转码器,可以将ES6代码转为ES5代码,从而在现有环境执行。 Babel总共分为三个阶段:解析(parse),转换(transform),生成(generate)。 Babel本身不具有任何转化功能,它把转化的功能都分解到一个个plugin里面。因此当我们不配置任何插件时,经过Babel输出的代码和输入是相同的。 Babel插件的使用 将插件的名字增加到配置文件中:项目根目录下创建.babelrc配置文件或是webapck.config.js中配置,一般都是在.babelrc中配置。使用 npm install xxx 进行安装Babel的配置文件是.babelrc,存放在项目的根目录下。使用Babel的第一步,就是配置这个文件。 该文件用来设置转码规则和插件,基本格式如下。 { "presets": [], "plugins": []}Babel简单介绍preset preset(预设)就是一系列插件的集合@babel/preset-env包含所有ES6转译为ES5的插件集合 core-js 转换一些内置类(Promise, Symbols等等)和静态方法(Array.from等)。 @babel/core 是作为Babel的核心存在,Babel的核心api都在这个模块里面。 babel-loader babel-loader在webpack中使用,是webpack和Babel之间的通讯桥梁 @babel/polyfill介绍 @babel/preset-env默认只转译js语法,而不转译新的API,比如Iterator、Generator、Set、Maps、Proxy、Reflect、Symbol、Promise等全局对象,以及一些定义在全局对象上的方法(比如Object.assign)都不会转译。这时就必须使用@babel/polyfill(内部集成了core-js和regenerator)。 使用时,在所有代码运行之前增加import "@babel/polyfill" 或者是在webpack.config.js入口配置 module.exports = { entry: ["@babel/polyfill", "./app/js"],}因此必须把@babel/polyfill作为dependencies而不是devDependencies @babel/polyfill主要有两个缺点: 1.使用@babel/polyfill需要做些额外配置,实现打包的时候按需引入,否则会把@babel/polyfill全部注入代码中会导致打出来的包非常大。 2.@babel/polyfill会污染全局变量。 Babel7的一个重大变化就是npm package 名称的变化,把所有babel-*重命名为@babel/*,例如: babel-polyfill重命名为@babel/polyfillbabel-preset-env重命名为@babel/preset-envBabel在webpack中的用法首先实现对ES6语法的转译 安装babel-loader、 @babel/core、@babel/preset-env npm i babel-loader -Dnpm i @babel/core -Dnpm i @babel/preset-env -Dbabel-loader@8需要安装@babel/core7.x版本。 在webpack.config.js配置 module.exports={ module: { rules:[ { test: /\.js$/, exclude: /node_modules/, use:{ loader: 'babel-loader', options:{ presets: [ ["@babel/preset-env",{ //targets:表示编译出的代码想要支持的浏览器版本 targets: { chrome: "67" } }] ] } } } ] }}执行npm run build或npx webpack就可以看到dist目录下的打包文件,但是只是将ES6的语法进行转译,并没有对ES6新API进行转译,所以我们需要配置@babel/polyfill解决这个问题。 ...

April 24, 2019 · 2 min · jiezi

webpack深入浅出系列1

webpack深入浅出系列--1--

April 23, 2019 · 1 min · jiezi

package-lock.json的作用

其实用一句话来概括很简单,就是锁定安装时的包的版本号,并且需要上传到git,以保证其他人在npm install时大家的依赖能保证一致。 引用知乎@周载南的回答 根据官方文档,这个package-lock.json 是在 npm install时候生成一份文件,用以记录当前状态下实际安装的各个npm package的具体来源和版本号。它有什么用呢?因为npm是一个用于管理package之间依赖关系的管理器,它允许开发者在pacakge.json中间标出自己项目对npm各库包的依赖。你可以选择以如下方式来标明自己所需要库包的版本 这里举个例子: "dependencies": { "@types/node": "^8.0.33",}, 这里面的 向上标号^是定义了向后(新)兼容依赖,指如果 types/node的版本是超过8.0.33,并在大版本号(8)上相同,就允许下载最新版本的 types/node库包,例如实际上可能运行npm install时候下载的具体版本是8.0.35。波浪号 大多数情况这种向新兼容依赖下载最新库包的时候都没有问题,可是因为npm是开源世界,各库包的版本语义可能并不相同,有的库包开发者并不遵守严格这一原则:相同大版本号的同一个库包,其接口符合兼容要求。这时候用户就很头疼了:在完全相同的一个nodejs的代码库,在不同时间或者不同npm下载源之下,下到的各依赖库包版本可能有所不同,因此其依赖库包行为特征也不同有时候甚至完全不兼容。 因此npm最新的版本就开始提供自动生成package-lock.json功能,为的是让开发者知道只要你保存了源文件,到一个新的机器上、或者新的下载源,只要按照这个package-lock.json所标示的具体版本下载依赖库包,就能确保所有库包与你上次安装的完全一样。 原来package.json文件只能锁定大版本,也就是版本号的第一位,并不能锁定后面的小版本,你每次npm install都是拉取的该大版本下的最新的版本,为了稳定性考虑我们几乎是不敢随意升级依赖包的,这将导致多出来很多工作量,测试/适配等,所以package-lock.json文件出来了,当你每次安装一个依赖的时候就锁定在你安装的这个版本。 那如果我们安装时的包有bug,后面需要更新怎么办? 在以前可能就是直接改package.json里面的版本,然后再npm install了,但是5版本后就不支持这样做了,因为版本已经锁定在package-lock.json里了,所以我们只能npm install xxx@x.x.x 这样去更新我们的依赖,然后package-lock.json也能随之更新。 假如我已经安装了jquery 2.1.4这个版本,从git更新了package.json和package-lock.json,我npm install能覆盖掉node_modules里面的依赖吗? 其实我也有这个疑问,所以做了测试,在直接更新package.json和package-loc.json这两个文件后,npm install是可以直接覆盖掉原先的版本的,所以在协作开发时,这两个文件如果有更新,你的开发环境应该npm install一下才对。

April 22, 2019 · 1 min · jiezi

webpack4.0入门学习笔记(二)

代码下载:github html-webpack-plugin的使用安装npm i html-webpack-plugin -D在webpack4.0入门学习笔记(一)中,我们是自己在打包目录下创建index.html对打包后js文件进行引用。 html-webpack-plugin插件可以根据对应的模板在打包的过程中自动生成index.html,并且能够对打包的文件自动引入。 在webpack.config.js的plugins中配置如下 const path = require('path')const htmlWebpackPlugin = require('html-webpack-plugin')module.exports={ entry: { main: './src/index.js' }, //打包完成后文件存放位置配置 output: { filename: 'bundle.js', path: path.resolve(__dirname,'dist') }, plugins: [ new htmlWebpackPlugin({ template: './index.html' }) ]}在终端执行npm run start,打包完成后在dist目录下自动生成index.html文件,并且还自动引入所有文件。 clean-webpack-plugin的使用每次打包生成的dist目录,如果改一次代码,都得要删除一次dist目录,这样很麻烦,可以通过clean-webpack-plugin在每次打包的时候自动清空dist目录。 安装npm i clean-webpack-plugin -D在webpack.config.js的plugins中配置如下 const path = require('path')const htmlWebpackPlugin = require('html-webpack-plugin')const cleanWebpackPlugin = require('clean-webpack-plugin')module.exports={ entry: { main: './src/index.js' }, //打包完成后文件存放位置配置 output: { filename: 'bundle.js', path: path.resolve(__dirname,'dist') }, plugins: [ new htmlWebpackPlugin({ template: './index.html' }), new cleanWebpackPlugin() ]}

April 21, 2019 · 1 min · jiezi

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

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

April 20, 2019 · 1 min · jiezi

避免CDN引用资源被webpack打包进出口bundle.js文件

解决cdn不做任何配置也同样被打包进webpack的出口文件的问题先说说为什么使用CDN打包事件太长,打包1代码体积太大,请求慢服务器不稳定带宽不高,使用CDN可以回避服务器带宽问题资源优化解决方案使用externals配置项: 防止将某些import包打包到bundle中,而在运行时再去外部获取扩展依赖·拿jQuery来举例1. 从CDN引入jQueryjavascript&lt;script src="https://code.jquery.com/jquery-3.1.0.js" integrity="sha256-slogkvB1K3VOkzAI8QITxV3VzpOnkeNVsKvtkYLMjfk=" crossorigin="anonymous"&gt;&lt;/script&gt;2. webpack.config.js配置javascript // .... externals: { jquery: 'jQuery' }3. 这样就解决了那些不需要改动的依赖具有外部依赖(external dependency)的 bundle 可以在各种模块上下文(module context)中使用javascriptimport $ from 'jquery';$('.my-element').animate(/* ... */);对于通过externals设置的外部依赖,可以通过以下几种方式来进行访问root 全局访问commonJS模块访问AMD模块访问

April 19, 2019 · 1 min · jiezi

搭建 React + webpack 项目错误记录

1、Cannot find module ‘webpack-cli/bin/config-yargs’webpack安装配置完执行"webpack-dev-server –devtool eval –progress –colors –content-base build"命令显示如上错误,差了一下发现是由于webpack与webpack-dev-server版本不兼容导致的问题,因此可将webpack及webpack-dev-server 写在卸载后重新指定版本安装。// 卸载当前版本npm uninstall webpack -gnpm uninstall -g webpack-dev-server// 安装指定版本npm install webpack@3.8.0 –save-devnpm install webpack-dev-server@2.9.7 –save-dev2、webpack4 Cannot find module ‘@babel/core’配置完webpack执行npm run dev,显示找不到@babel/core这个module,执行以下命令安装:npm install –save-dev @babel/core

April 19, 2019 · 1 min · jiezi

Homestead + laravel-mix 环境下 hmr 的两种玩法

博客原文我在前几天刚写过的《让 F5 歇一会儿——laravel-mix 自动刷新之道》中介绍了 laravel-mix 实现自动刷新的几种方法,其中就有涉及 hmr(Hot Module Replacement),但里面都是以 Laradock 环境为例。对于 Laravel 官方首推的 Homestead 当然也是可以的,只不过用法上有些差别,于加上 laravel-mix 本身的一些 BUG(在 issue 里搜索 hmr 结果就有好几页 ),对于刚接触的人来说可能无从下手。本文介绍两种不同的玩法。首先假定你已经创建了一个 laravel 项目,进行了相关配置(.env 配置及绑定测试域名,如:laravel.test)并已装好了后端依赖玩法一:使用虚拟机中的 Node 环境因为 Homestead 提供的环境里默认包含了前端开发所需要的 Node 环境及相关工具(gulp, npm, yarn 等),所以直接使用它们似乎是很省事的选择。vagrant ssh 连接虚拟机,进入项目目录后安装前端依赖yarn install在 webpack.mix.js 中调整相关配置使用 mix.Webpack() 配置 devServermix.webpackConfig({ devServer: { watchOptions: { poll: 2000, // 这个值可调整,性能高的时候可以调小,也可以直接设置为 true ignored: /node_modules/, }, },})> 这一配置很关键,因为要是仅使用 devServer 的默认 watch 选项,对于虚拟机环境是无效的(见 webpack 文档)- 调整 hmrOptions js mix.options({ hmrOptions: { host: 'laravel.test', port: 8080 } }) 在虚拟机终端中执行yarn run hot,然后在浏览器中使用绑定的测试域名(如:laravel.test)访问修改 JS 等,自动编译后浏览器中页面即自动更新玩法二:使用宿主机中的 Node 环境当然也可以使用宿主机的 Node 环境,对于开发都来说,这些环境应该也是必须的了。从宿主机终端进入项目目录并安装前端依赖yarn installwebpack.mix.js 中使用 webpackConfig 进行配置mix.webpackConfig({ devServer: { disableHostCheck: true, }, // 其它配置})disableHostCheck: true 是为了避免出现下面这种错误。与玩法一中不一样,不再需要特别在 hmrOptions 中指定 devServer 和 host 和 port,使用默认的就好(事实上也不能像前面那样指定,因为会出现 IP/端口 冲突)在宿主机终端中执行yarn run hot,然后在浏览器中使用绑定的测试域名(如:laravel.test)访问修改 JS 等,自动编译后浏览器中页面即自动更新总结两种方法并没有谁好谁坏之分,具体使用哪种方法视具体场景及个人喜好而定。就我个人而言,通常使用第二种,主要原因有二:一是出于性能/延迟方面的考虑,因为在虚拟机中使用轮询(poll)的方式来监听文件变化,当 poll 设置间隔较大时可能会出现一定延迟,而设置太小轮询太频繁则又可能造成一定的性能压力。所以直接使用宿主机的 Node 环境似乎更为划算。二是自己使用的 IDE(PhpStorm)运行在宿主机(Windows)中,而 PhpStorm 的一些插件(或服务)如 Eslint、TypeScript、 Prettier 需要使用使用本地安装的一些 npm 包,这样就只能在宿主环境里安装依赖。(虽然可以考虑在宿主机全局安装依赖,但诸如 eslint-config-xxx 之类的项目相关的包也全局安装,必然造成混乱)如同学习很多其它新工具新玩法一样,刚开始折腾 laravel-mix 时总是磕磕绊绊(有不少坑),但一旦掌握了窍门,就能极大的方便日常开发,提高工作效率。博客里记下这些,权当备忘,也算是分享,独乐不如众乐。 ...

April 19, 2019 · 1 min · jiezi

Vue项目构建持续集成阿里云CDN

CDN加速是Web应用性能优化和用户体验提升的至关重要的一环,当一个项目构建部署时,就需要考虑到如何高效的去完成相关资源的CDN部署。本文以一个基于 vue-cli3 构建的项目实例,来简单讲解如何配合Teamcity,自动进行阿里云CDN资源部署和持续集成。项目构建vue-cli3 默认支持将项目以 test、development、production 三种模式构建,其中 production 模式将在 build 后生成 dist目录。我们在项目路径下插入 .env.[mode] 格式的文件就可以实现自定义模式。通常,默认的构建模式无法满足项目研发需求。一个项目至少需要包含本地调试 - 即开发过程中的 development 模式,不生成 dist 静态目录,使用 vue-dev-server运行项目;测试环境 - 即基本的集成测试,需要文件静态化,部署到测试环境;线上环境 - 即用户环境,也需要文件静态化,并做CDN加速等性能优化措施;按照这个模型,我们需要自定义一个 deploy 模式,来实现和普通 production打包后,资源引入路径的区别。首先,环境创建在项目根目录下创建 .env.deploy 文件,添加内容如下:NODE_ENV=productionDEPLOY=onlineNODE_ENV的设置代表webpack构建时使用production模式,即会生成 dist静态目录。DEPLOY的设置,是一个我们定义的变量,用于在配置中区分deploy和production模式。其次,配置文件在 vue.config.js 中,配置 BASE_URL// 根据自定义的变量来进行内容设置let BASE_URL = ‘/‘switch(process.env.DEPLOY) { case ‘online’: BASE_URL = ‘http://web-cdn.xxx.com/' break default: BASE_URL = ‘/’}module.exports = { publicPath: BASE_URL, ….}该配置会使得当程序使用 deploy 模式运行时,打包的资源根路径为我们的CDN地址。最后,构建命令在 package.json 中,配置使用 deploy 模式的打包命令"scripts": { “build”: “vue-cli-service build”, “deploy”: “vue-cli-service build –mode deploy”, …}当用户执行 npm run build 时,会生成以 / 为资源路径的文件;当用户执行 npm run deploy 时,生成 index.html 中的资源路径就变成了我们配置的CDN路径。<!DOCTYPE html><html> <head> <meta charset=utf-8> <meta http-equiv=X-UA-Compatible content=“IE=edge”> <meta name=viewport content=“width=device-width,initial-scale=1”> <link rel=icon href=http://web-cdn.xxx.com/favicon.ico> <title>Demo</title> <link href=http://web-cdn.xxx.com/css/chunk-0fabbc4c.08fa0fd2.css rel=prefetch> <link href=http://web-cdn.xxx.com/css/chunk-1025f268.0dc416de.css rel=prefetch> <link href=http://web-cdn.xxx.com/js/app.84dcc9e6.js rel=preload as=script> </head> <body> <div id=app></div> <script src=http://web-cdn.xxx.com/js/chunk-vendors.614ecc0c.js></script> <script src=http://web-cdn.xxx.com/js/app.84dcc9e6.js></script> </body></html>阿里云CDN配置和上传接下来,我们要做的就是配置一个CDN,并能够把这些资源传上去。首先,在阿里云上配置CDN,做好域名CNAME解析,并获取到阿里云的 accessKeyId、accessKeySecret、Region、BucketName等信息,然后选择一种语言,写好上传脚本。这里我们以Node脚本为例:// oss-deploy.jslet OSS = require(‘ali-oss’)let fs = require(‘fs’)let client = new OSS({ region: ‘oss-cn-hangzhou’, accessKeyId: ‘xxx’, accessKeySecret: ‘xxx’, bucket: ‘xxx’})// 使用async+await方法,实现同步化,方便在失败后重试处理async function put(fileName) { try { let result = await client.put(fileName, ‘../dist/’ + fileName) console.log(‘File Upload Success: ‘, fileName) } catch (e) { console.log(‘File Upload Failed: ‘, fileName) // 这里省略异常/失败的重试 }}// 读取打包后的 dist 路径,按照原文件夹结构,进行上传let readFileList = (path, filesList) => { let files = fs.readdirSync(path) files.forEach(itm => { if (itm) { let stat = fs.statSync(path + itm) if (stat.isDirectory()) { readFileList(path + itm + ‘/’, filesList) } else { filesList.push(path + itm) } } }) return filesList}let dist = readFileList(’../dist/’, [])// 递归执行文件上传操作let i = 0, l = dist.lengthlet uploadAsset = () => { if (i < l) { let name = dist[i].split(’../dist/’)[1] put(name) i++ uploadAsset() }}uploadAsset()执行npm install –save-dev ali-ossnode oss-deploy.js即可看到文件已经被上传到了CDN路径下。持续集成上面的两个模块,已经实现了基本的CDN部署。但我们在项目开发的时候,肯定不希望每次 build完,都去自己执行上传CDN,再去服务器上部署。这里我们再把 TeamCity上实现自动build、一键上线的流程简单阐述。TeamCity上的执行脚本如下:cd /apps/kaleido-cms/git pull -f origin masternpm installnpm run deploygit add dist/*git commit -m “Deploy"git push origin mastercd /apps/kaleido-cms/deploynode oss-deploy.jsssh root@10.0.0.1 “./deploy_cms.sh"ssh root@10.0.0.2 “./deploy_cms.sh"因为线上服务通常是集群模式,而 webpack在不同服务器执行build,会产生不同的哈希值版本号,会导致远程资源无法获取到。所以我们需要在持续集成部署的服务器上做build操作,生成dist路径,上传到git和cdn。最后再到集群的每个服务器上拉取静态文件即可。补充:在同一台服务器上,只要文件完全不变,我们使用vue-cli3构建生成的最终文件的哈希值版本号就不会产生改变。因此,对于用户来说当我们更新版本时,并不会对用户造成所有缓存文件失效的性能和体验影响。在阿里云的CDN上,是使用协商缓存的ETag来进行文件资源缓存,因此重名新文件覆盖旧文件时,如文件内容完全一致,Etag也会保持一致,对用户来讲也不必担心缓存问题;如文件发生变更,用户协商缓存也将无法命中,就会取新的资源文件。有些方法是把静态资源的请求发到Nginx,然后再转发到CDN地址。笔者认为,这样会造成所有资源需要重定向、并且在Nginx上无法设置缓存信息,性能上不如本文介绍的直接构建生成CDN地址的HTML文件的方法。通过这套操作,最终我们实现了在TeamCity上,一键执行打包、上传CDN、部署的整个流程。 ...

April 18, 2019 · 2 min · jiezi

项目:(MPA应用)企业展示 目录

项目框架基础框架:https://segmentfault.com/a/11…使用之前 webpack 搭建的框架。项目源码github 地址:https://github.com/Zhanghongw…页面开发地址:https://segmentfault.com/a/11…

April 18, 2019 · 1 min · jiezi

从0开始配置webpack和搭建一个React项目

先来说说react搭建: 1 官方文档上也有提供直接下载react包,但是修改webpack配置比较麻烦npx create-react-app my-appcd my-appnpm start修改webpack配置需要执行npm run eject2 自行搭建一个项目并且配置webpack–主要记录学习阶段~总结的可能不太好,勉强看看,重点记录一下第二种的方式通过yarn管理包 下载yarnyarn官网链接安装步骤都有的在项目目录下,执行yarn init会出现我们的package.json文件安装webpackyarn add webpack –dev新建webpack.config.js文件,贴官网示例: const path = require(‘path’); module.exports = { entry: ‘./src/app.js’, output: { path: path.resolve(__dirname, ‘dist’), filename: ‘app.js’ } };命令行执行webpack会发现dist目录 注意:yarn安装过程中如果出错,尝试将yarn切换到淘宝镜像再进行下载哦~,我安装过程中出现过问题,切到这就没问题了 yarn config set registry ‘https://registry.npm.taobao.org’安装html-webpack-pluginyarn add html-webpack-plugin –dev文档使用链接地址 按照文档操作,修改webpack.config.js使用html-webpack-plugin打包html文件 再次执行webpack命令,会发现dist文件夹下面多了一个index.html 设置html-webpack-plugin的template模版,在src新建index.html,并且设置html内容 const path = require(‘path’); var HtmlWebpackPlugin = require(‘html-webpack-plugin’); module.exports = { entry: ‘./src/app.js’, output: { path: path.resolve(__dirname, ‘dist’), filename: ‘app.js’ }, plugins: [new HtmlWebpackPlugin( { template:’./src/index.html’ } )] };现在dist文档下面的index.html就是当前src下的index.html的模版了安装babelyarn add babel-loader @babel/core @babel/preset-env具体详情见文档地址 在src/app.js中写入一些ES6语法,再次执行webpack命令,dist/app.js进行了转换安装react转换 babel-preset-reactyarn add babel-preset-react –dev修改webpack.config.js const path = require(‘path’); var HtmlWebpackPlugin = require(‘html-webpack-plugin’); module.exports = { entry: ‘./src/app.jsx’ path: path.resolve(__dirname, ‘dist’), filename: ‘app.js’ }, plugins: [new HtmlWebpackPlugin( { template:’./src/index.html’ } )], module: { rules: [ { test: /.m?jsx$/, exclude: /(node_modules|bower_components)/, use: { loader: ‘babel-loader’, options: { presets: [’@babel/preset-env’,‘react’] } } } ] } };安装react yarn add react react-domreact添加操作地址详情 将src/app.js修改为app.jsx import React from ‘react’; import ReactDOM from ‘react-dom’; ReactDOM.render( <h1>Hello, world!</h1>, document.getElementById(‘app’) );再执行webpack进行打包 如果出现Error: Plugin/Preset files are not allowed to export objects, only functions.错误说明babel的版本不一致,我这边是因为"babel-preset-react": “^6.24.1"默认装的6版本,其他babel安装的是7版本,所以统一升级到7或者降级到6yarn add babel-preset-react@7.0.0 –dev这样在进行打包,就可以了,这个时候打开dist/index.html我们看到hello, world!说成功编译了react安装style-loaderyarn add css-loader style-loader –dev安装地址操作详情 在webpack.config.js的rules中添加 { test: /.css$/, use: [‘style-loader’, ‘css-loader’], },在src下新建一个文件index.css,随便修改一点样式 h1{ color:#F00; }在app.jsx中引入 import ‘./index.css’再次执行webpack打包,刷新dist/index.html安装ExtractTextWebpackPlugin插件将css独立到单独的文件 yarn add extract-text-webpack-plugin –dev官网链接地址 const path = require(‘path’); var HtmlWebpackPlugin = require(‘html-webpack-plugin’); const ExtractTextPlugin = require(“extract-text-webpack-plugin”); module.exports = { entry: ‘./src/app.jsx’, output: { path: path.resolve(__dirname, ‘dist’), filename: ‘app.js’ }, module: { rules: [ { test: /.m?jsx$/, exclude: /(node_modules|bower_components)/, use: { loader: ‘babel-loader’, options: { presets: [’@babel/preset-env’,‘react’] } } }, { test: /.css$/, use: ExtractTextPlugin.extract({ fallback: “style-loader”, use: “css-loader” }) }, ] }, plugins: [ new HtmlWebpackPlugin( { template:’./src/index.html’ } ), new ExtractTextPlugin(“index.css”), ], };webpack.config.js配置如上 再次执行webpack,dist目录下就多了一个index.css了~ 注意:打包遇到Tapable.plugin is deprecated. Use new API on .hooks instead错误,原因是extract-text-webpack-plugin目前版本不支持webpack4 执行:yarn add extract-text-webpack-plugin@next –dev安装sass-loader yarn add sass-loader –dev在webpack.config.js中rules添加 { test: /.scss$/, use: ExtractTextPlugin.extract({ fallback: ‘style-loader’, use: [‘css-loader’, ‘sass-loader’] }) }新建一个index.scss文件 body{ background: #ccc; #app{ font-size: 22px; } }在执行webpack会出现报错Cannot find module ’node-sass’查看文档链接 需要安装node-sass yarn add node-sass –dev打包,查看index.html可以看到样式应用上去了~安装url-loader处理图片链接 yarn add url-loader file-loader –dev官网地址 在rules中加入: { test: /.(png|jpg|gif)$/i, use: [ { loader: ‘url-loader’, options: { limit: 8192 } } ] }项目中引入图片,进行打包,这样图片资源也打包解析进去了~添加解析字体rule { test: /.(eot|svg|ttf|woff|woff2|otf)$/i, use: [ { loader: ‘url-loader’, options: { limit: 8192, name:‘resource/[name].[ext]’ } } ] },添加webpack-dev-server yarn add webpack-dev-server –dev修改package.json添加 “scripts”: { “test”: “echo “Error: no test specified” && exit 1”, “watch”: “webpack –watch”, “start”: “webpack-dev-server –open”, “build”: “webpack-cli” } 执行yarn run start启动项目yarn run build打包项目最后附上当前为止修改后的webpack.config.js const path = require(‘path’); const webpack = require(‘webpack’); var HtmlWebpackPlugin = require(‘html-webpack-plugin’); const ExtractTextPlugin = require(“extract-text-webpack-plugin”); module.exports = { entry: ‘./src/app.jsx’, output: { path: path.resolve(__dirname, ‘dist’), filename: ‘./js/[name].[hash].js’, chunkFilename: ‘./js/[name].[hash].js’, }, devServer: { port: 8080, proxy: { ‘/expo’: { target: ‘https://xxx’, changeOrigin: true, pathRewrite: { ‘/expo’: ‘/expo’, }, secure: false, }, }, hot:true }, module: { rules: [ { test: /.m?jsx$/, exclude: /(node_modules|bower_components)/, use: { loader: ‘babel-loader’, options: { presets: [’@babel/preset-env’,‘react’] } } }, { test: /.css$/, use: ExtractTextPlugin.extract({ fallback: “style-loader”, use: “css-loader” }) }, { test: /.scss$/, use: ExtractTextPlugin.extract({ fallback: ‘style-loader’, use: [‘css-loader’, ‘sass-loader’] }) }, { test: /.(png|jpg|gif|ico|jpeg)$/i, use: [ { loader: ‘url-loader’, options: { limit: 8192, name: “[name].[ext]”, publicPath: “../images/”, outputPath: “images/” } } ] }, { test: /.(eot|svg|ttf|woff|woff2|otf)$/i, use: [{ loader: “file-loader”, options: { name: “[name].[ext]”, publicPath: “../fonts/”, outputPath: “fonts/” } }] }, ] }, plugins: [ new HtmlWebpackPlugin( { template:’./src/index.html’ } ), new ExtractTextPlugin(“css/[name].css”), ], optimization:{ splitChunks:{ name:‘common’, filename:‘js/base.js’ } } }; ...

April 18, 2019 · 3 min · jiezi

从零开始开发一个react脚手架(四)

这一篇可能主要讲的是热更新,写的很细,遇到很多有意思的地方,一一和大家讲解下。前沿:webpack-dev-server支持热更新,简单的说就是你修改代码,浏览器能够自动刷新页面。因为很久没有配置过webpack-dev-server,搭建出效果后,我一直以为是错的,因为在我的理解上,我以为的热更新并非是刷新页面,而是更新改动的模块。要实现非刷新页面的热更新,还需要有额外的逻辑。先看一段代码截图webpack-dev-server的配置。historyApiFallback设置为true,有点类似于app.get("*", index),就是一个兜底的路由,保证所有未拦截的404页面都转向index。contentBase 设置的就是dist目录,即webpack打包的dist目录,所以开启webpack-dev-server之前,必须打包一下,不然找不到index.html文件。hot设置为true,表示启用热更新,因为我们用的是API方式允许webpack-dev-server,所以配置项目中必须设置port和host,否则会报错。重点:基本配置完成后,在增加一段代码到webpack的entry里面因为我们走的是API,而webpackDevServer里面已经提供了以方法addDevServerEntrypoints实现。只需要传递两个配置参数即可。他的效果如图,我打印出了webpackConfig说白了就是手动把热更新的两个JS文件插入到了entry中,一并打包。如果我们手动写的webpack.config.js,就应该明白这点。所以这个API还是很方便的。其实走到这里就能实现页面自动刷了。but…根据配置经验,还需要配置一个plugins.push(new webpack.HotModuleReplacementPlugin());but,根据我实际的测试结果,不用手动加入这个plugin也可以实现热更新。原因就跟我上面说的一样,API自动加上了这个配置。我们公司的脚手架没用这个API结果,导致自己额外增添了很多配置。BUT,走到这里,我们会发现只实现了第一步页面自动刷新。如果我们开发的是react应用就远远不够了。因为一旦项目大起来,刷新页面将会是一件非常非常耗时的事情,尤其是当涉及到服务器端渲染的时候。要实现类似于懒更新的功能,需要引入react-hot-loader。引入最新版本,根据文档,只需要配置两个地方即可。 脚手架的babel配置,增加一个plugin react-hot-loader/babel然后在我们的项目目录中cli-view 中包裹一层Root.jsx至此就能完美的实现开发环境的自动的更新了,更改代码,能够实现刷新当前更改的module,而不是刷新整个页面。 其实还有一个小小的疑问,在测试过程中,我即便不加上 react-hot-loader/babel这个plugin,也能够实现懒更新,只需要在项目目录中配置即可。看了下这个plugin的源码,没看出所以然来,我猜测这个plugin,是不是说懒启动的时候,保证能走一遍babel编译? 有待大佬验证!!!。顺便简单说下proxy,一般而言调用后台接口都会报跨域,但设置了proxy,类似于在node层做了一次服务转发。我把原本cli-view目录下的webpack.config.js改成了app.config.js。我把所有的配置都放在了这个文件里面。我本地启用了一个端口8888的服务,而我的cli-view的port是3000,当我请求API后,所有的/api前缀的请求都转到了8888下。到了这里关于webpack-dev-server的内容就差不多了。很细,很有意思

April 13, 2019 · 1 min · jiezi

npm ERR! Invalid name

$ npm init -ynpm ERR! Invalid name: “03 install methods webpack"npm ERR! A complete log of this run can be found in:npm ERR! C:\Users\Administrator\AppData\Roaming\npm-cache_logs\2019-04-13T09_18_06_413Z-debug.log报错原因:在"name"字段中不允许使用 大写字母、空格和中文 package.json。

April 13, 2019 · 1 min · jiezi

使用 Webpack1.x 搭建 (MultiPage Application,MPA) 基础框架

初始化项目webpack官方文档:https://www.webpackjs.com/con…github项目地址:https://github.com/Zhanghongw…项目搭建简单记录一下命令,网上有很多文章不多做介绍。希望可以提供一个基础的架构。持续更新……..执行命令// 全局安装 webapck、webpack-cli, 之前安装过忽略此步骤npm install webpack -g npm install webpack-cli -g初始化 npmnpm init项目目录结构+dist+src++assets+++images++common++page++view安装相关依赖,注意版本npm install xxx@版本号 –save-dev{ “css-loader”: “^2.1.1”, “extract-text-webpack-plugin”: “^1.0.1”, “html-loader”: “^0.4.5”, “html-webpack-plugin”: “^2.28.0”, “style-loader”: “^0.23.1”, “url-loader”: “^0.5.8”, “webpack”: “^1.15.0”, “webpack-dev-server”: “^1.16.5” }webpack.config.js 配置文件var path = require(‘path’);var webpack = require(‘webpack’);var ExtractTextPlugin = require(’extract-text-webpack-plugin’);var HtmlWebpackPlugin = require(‘html-webpack-plugin’);// 获取html-webpack-plugin参数的方法 var getHtmlConfig = function(name){ return { template : ‘./src/view/’ + name + ‘.html’, filename : ‘view/’ + name + ‘.html’, inject : true, hash : true, chunks : [‘common’, name] };};var config = { entry: { ‘common’:[’./src/common/index.js’], ‘index’:[’./src/page/index/index.js’], ‘home’:[’./src/page/home/home.js’] }, output: { path: path.resolve(__dirname, “dist”), publicPath: “/dist”, filename: ‘js/[name].js’ }, module: { loaders: [ // 处理 css { test: /.css$/, loader: ExtractTextPlugin.extract(“style-loader”,“css-loader”) }, // 处理图片 { test: /.(gif|png|jpg|jpeg)??.*$/, loader: ‘url-loader?limit=100&name=resoure/[name].[ext]’ } ] }, plugins: [ new webpack.optimize.CommonsChunkPlugin({ // 公共模块提取 name : ‘common’, filename : ‘js/base.js’ }), // 把css单独打包到文件里 new ExtractTextPlugin(“css/[name].css”), // html 模板处理 new HtmlWebpackPlugin(getHtmlConfig(‘index’)), new HtmlWebpackPlugin(getHtmlConfig(‘home’)) ]};module.exports = config; ...

April 13, 2019 · 1 min · jiezi

webpack学习-渐进开发和优化

webpack静态模块打包工具,将js、css、img等静态资源按依赖关系构建关系网。最近接手的一个很简单的单页面,刚好使用webpack来看看webpack有多么的神器。

April 12, 2019 · 1 min · jiezi

关于DllPlugin的一些理解

以一个vue全家桶为例 entry: { vendor: [ ‘vue/dist/vue.esm.js’, ‘vue-router’, ‘vuex’ ] }图1为在生产环境中未使用DllPlugin生成的chunk.js文件图2为在生产环境中使用DllPlugin生成的.dll.js文件图3为在开发环境中未使用DllPlugin生成的.esm运行时文件可以看到,三个文件中都有isUnknownElement这个方法,这说明使用DllPlugin生成的.dll.js文件的内容既为.esm运行时文件的内容(另外这里的.esm运行时文件相当于一个polyfill,内置了一些方法,用于解析webpack编译vue后生成的.js、.css等文件)在生产环境下,当不使用DllPlugin时,每次编译都会把.esm运行时文件编译到chunk.js中。使用DllPlugin后,只要在index.html中主入口文件前加载这个dll.js文件,就能拿到esm运行时文件的内容,省去了每次编译vue全家桶的时间<!DOCTYPE html><html lang=“en”> <head> <meta charset=“utf-8”> <title>dll-test</title> </head> <body> <div id=“app”></div> <script src="./static/js/vendor.dll.js"></script> <script src="./main.js"></script> </body></html>使用场景: 1.对于像vue,react全家桶这样组合,推荐使用DllPlugin。因为这些框架普遍比较大,而且经常需要组合在一起使用。使用DllPlugin在时间上会有比较不错的收益2.对于lodash、jquery这样的第三方库,不推荐使用DllPlugin。jquery推荐使用ProvidePlugin。lodash通过es6 import语法导入,并配置untils选项可以实现按需加载

April 10, 2019 · 1 min · jiezi

A fascinating technique that can greatly reduce your code

It’s a common way in most web developers that write JavaScript with ES6+ and then bundle it to ES5, so that it can run in all browsers. However, the modern browsers support ES6 natively so it’s unnecessary to shipping a lot of polyfills.I’m really excited to share you a technique that you can compile and serve two separate JavaScript bundles:One bundle you are definitely already generating, which serve for legacy browsers with polyfills.Another bundle has less code with no polyfills, which serve for modern browsers.That is <script type=“module”> as a way to load ES modules. You can also load code with <script nomodule> for legacy browsers.The rest of this article explains how to implement this technique and the bundle solution for webpack.ConceptsHow does it works? let’s look at a example:<!– For modern browsers –><script type=“module” src=“main.mjs”></script><!– For legacy browsers –><script nomodule src=“main.js”></script>In modern browsers, script with type=“module” will be loaded and executed, and script with nomodule will be ignored.And in legacy browsers, script with type=“module” will be ignored because they can’t recognize this attrbute, script with nomodule has no effect, it will be treated as usual.Note: There’s something you should know about <script type=“module”>.It isn’t executed until the document has been parsed, just like <script defer>.Code is running in strict mode and top-level isn’t window.Warning: Safari 10 doesn’t support the nomodule attribute, but you can solve this by inlining a JavaScript snippet in your HTML (This has been fixed in Safari 11).ImplementationI really appreciate it that @babel/preset-env provides a convenient config for esmodules.babel for legacy:{ “presets”: [ [ “@babel/preset-env”, { “modules”: false, “useBuiltIns”: “entry”, “targets”: { “browsers”: [ “> 1%”, “last 2 versions”, “Firefox ESR” ] } } ] ]}babel for modern:{ “presets”: [ [ “@babel/preset-env”, { “modules”: false, “useBuiltIns”: false, “targets”: { “esmodules”: true } } ] ]}It’s a bit complex for webpack to bundle two different JavaScript, there are some details you should know. So I wrote a esmodules-webpack-plugin to simplify configuration and bundle thest JavaScript just run webpack once.Is it worth a try?I think it’s definitely worth a try, the size of polyfills that bundles by babel is more than what we think, and ES Modules code has an average reduction of 50% or even more, it all depends on your source code.Besides, larger files not only take longer to download, but also take longer to parse and execute. So reduce file size is a efficient way to improve the performance of website.Conclusion<script type=“module”> is really a fascinating technique that helps us shipping less code to users who use modern browsers and improve our website’s performance.However, there are also some limitations. for example, most module authors don’t publish ES6+ versions of their source code, a few browsers support <script type=“module”>, but will still download <script nomodule>(but won’t execute).ResourcesA convenient webpack plugin for esmodules that I wrote: esmodules-webpack-plugin ...

April 8, 2019 · 3 min · jiezi

从零开始开发一个react脚手架(三)

前面两篇文都只是铺垫,今天至少要实现一个简单react的hello word开始之前再说一下垫片和presets。 前几天突发疑问,create-react-app是怎么做的垫片,因为很多语法或者API不一定兼容所有浏览器,所以需要有垫片(polyfill)去帮我们做兼容。我一开始以为会在entry引入@babel/polyfill,但并不是。而是引入了一个babel-preset-react-app。如果有自己写webpack配置的经验,应该明白我们配置babel的时候,引入了许多的preset和一些plugins,比如@babel/preset-env,@babel/preset-react, @babel/plugin-proposal-object-rest-spread(支持对象展开符) @babel/plugin-syntax-dynamic-import(支持异步import语法)等等,还有很多。但是这个presets集合了很多很多preset,这样我们就没必要分别引入了。可以看到这个preset里面还有 @babel/runtime 这个就是垫片库了,结合这个 @babel/plugin-transform-runtime可以做到按需加载垫片库,具体和@babel/polyfill的差别就不说了,自己去查询文章。参考create-react-app,我简化了他的N多目录和结构,去掉了很多的兼容拓展。当然先只是简单实现了一个类似于npm run start。这是我的index.js,当然以后会拆出来,作为bin目录下的一个执行文件。cli-view当执行 node ./src/cs.js,就会进行打包构建了。会自动打开默认浏览器简单说下三个文件webpackConfig,devServerConfig,config。当然了应为还只是最初版本,很多细节功能压根没有。先说下config.js这个就是单纯的cache一些环境配置,当然我仍然依赖了一下webpack.config.js,其实这个是纯粹一个暴露在项目中的配置文件,它也可以叫其他名字例如app.config.js之类的。里面就配置了一个entry。create-react-app貌似默认去的就是src下的index.jsx,其实我们也可以这样玩,但我觉得暴露个entry的入口配置,灵活性更高点。毕竟我们还可以玩多页面。至于到底怎么整,大家可以自己取舍。createWebpack.js里面就是我们平常看到的配置文件了。这里有几个细节要提一下,1 比如写配置loader的时候,我一开始直接babel-loader,报错找不到babel-loader,我估摸着是它的逻辑是走到了项目目录下去找了,而我的项目目录没有安装这个,所以我按照create-react-app的来,加上了require.resove,估计走的就是脚手架的node_modules去找babel-loader,就没报错了。 2 babel的presets我也直接用的是babel-preset-react-app(其实就是几个preset的集合)3 写脚手架的时候稍微注意下目录,别搞混了,比如process.cwd(),__dirname之类的。createDevServer就最简单了,就是一个简单的serve配置了。但后续应该还要加上代理之类的。因为现在自己手上的项目都是引入了node,用不到webpack-dev-server。我估计用这个调用后台接口的时候,要么自己起nginx项目做转发,不然就在这里设置proxy。不然跨域。虽然看起来简单,但真正自己敲着代码来,还是会发现好多细节问题。因为功能还非常不完整,比如css,热加载都还没有就暂时不放到git上了。但下一期完善功能就放上去。下一期争取做到1 完善各种webpack基本配置2 hot热加载3 试着push到npm包管理,通过npm run start来启动项目。

April 7, 2019 · 1 min · jiezi

webpack插件 - css主题颜色提取-主题切换

css-color-extract-pluginInstallnpm install css-color-extract-pluginyarn add css-color-extract-plugin该插件主要用于提取主题颜色提取到的css数据会挂载到window下通过颜色替换再插入到<style>,可达到动态修改主题的目的Usage// webpack.config.jsconst CssColorExtractPlugin = require(‘css-color-extract-plugin’).default;const PRIMARY_COLOR = ‘#1890ff’;module.exports = { … module: { rules: [ { test: /.css$/, exclude: ‘/.module.css$/’, use: [ “style-loader”, “css-loader”, { loader: CssColorExtractPlugin.loader, options: { colors: [ PRIMARY_COLOR ] } }, ] }, { test: /.module.css$/, use: [ “style-loader”, { loader: “css-loader”, options: { modules: true, localIdentName: ‘[path][name][local]’, } }, { loader: CssColorExtractPlugin.loader, options: { colors: [ PRIMARY_COLOR ], modules: true, localIdentName: ‘[path][name][local]’, } }, ] } ] } … plugins: [ … new CssColorExtractPlugin({ fileName: ’theme’ }), ]};编译后会在html中插入theme.js,其内容类似以下window.CSS_EXTRACT_COLOR_PLUGIN = [ {“source”:".src-App-module__example { background: #1890ff;}",“fileName”:“App.module.scss”,“matchColors”:["#1890ff"]}, {“source”:".src-Header-module__example { color: #1890ff;}",“fileName”:“Header.module.scss”,“matchColors”:["#1890ff"]}];var styles = document.createElement(‘style’);styles.innerHTML = window.CSS_EXTRACT_COLOR_PLUGIN.map((item) => item.source).join(’’);document.body.appendChild(styles);接着只要使用简单的正则即可替换主题色import React, { Component } from ‘react’;import styles from ‘./App.module.scss’;import { SketchPicker } from ‘react-color’;function replaceColor(source, color, replaceColor) { return source.replace(new RegExp((:.*?\\s*)(${color})(\\b.*?)(?=}), ‘mig’), (group) => { return group.replace(new RegExp(${color}, ‘mig’), replaceColor); });}const PRIMARY_COLOR = ‘#1890ff’;class App extends Component { async setColor(color) { const styleData = window.CSS_EXTRACT_COLOR_PLUGIN || []; const cssText = styleData.map((item) => item.source).join(’’); const styleText = replaceColor(cssText, PRIMARY_COLOR, color); const style = document.createElement(‘style’); style.innerHTML = styleText; document.body.appendChild(style); } render() { return ( <div className={styles[’example’]}> <SketchPicker onChangeComplete={(colorResult) => this.setColor(colorResult.hex)} /> </div> ); }}export default App;loader Options { colors: string[]; // 匹配的颜色数组,如果出现颜色层次错误覆盖的情况,需要选上被覆盖的颜色,可通过该选项在不同的文件提取不同的颜色 only?: boolean = true; // 仅提取选中颜色规则,否则会将整个文件提取进去 modules?: boolean = false; localIdentName?: string = ‘’; }plugin Options { fileName?: string; // 提取颜色的文件名,不提供则直接嵌在 script标签中 variableName?: string = ‘CSS_EXTRACT_COLOR_PLUGIN’; // 挂载到window的变量名, 默认 CSS_EXTRACT_COLOR_PLUGIN }example一个更复杂的例子-RyanCMS内容管理系统 ...

April 5, 2019 · 2 min · jiezi

7.工程架构的优化

到这一章节,一个前端项目的工程架构基本已经搭建起来了,但是还有有需要优化的地方。webpack配置重复client端和server端的配置有重复的地方,优化的手段就是将两个文件中重复的配置项提取出来,然后利用webpack-merge包去合并配置。提取公共的配置到webpack.config.base.js文件const path = require(‘path’)function resolvePath(filePath) { return path.join(__dirname, filePath);}module.exports = { mode: ‘development’, resolve: { extensions: [’.js’,’.jsx’] }, output: { path: resolvePath(’../dist’), publicPath: ‘/public/’ }, module: { rules: [ { enforce: ‘pre’, test: /.jsx$/, loader: ’eslint-loader’, exclude: [ resolvePath(’../node_modules’) ] }, { test: /.jsx$/, loader: ‘babel-loader’ }, { test: /js$/, loader: ‘babel-loader’, exclude: [ resolvePath(’../node_modules’) ] } ] }}在wenpack.config.client和webpack.config.server文件中删除base文件中的配置,const webpackMerge = require(‘webpack-merge’)webpackMerge(baseConfig, {/不同的配置/})服务端的icon在润兴dev:server时,或有一个favicon.ico的请求,目前我们返回的html文件。可利用serve-favicon工具来解决这个问题。只需要创建一个ico文件,然后在server/server.js文件中增加几行代码即可。const favicon = require(‘serve-favicon’)app.use(favicon(path.join(__dirname, ‘../favicon.ico’))) // 服务端服务自动重启目前服务端的代码改动后,需要重新启动服务。利用nodemon,可以做到文件改动后,自动重启服务。安装nodemon,在根目录下创建nodemon.json文件{ “restartable”: “rs”, // 是否可以重启 “ignore”: [ // 忽略文件的变化 “.git”, “node_modules/**/node_modules”, “.eslint”, “slient”, “build” ], “env”: { “NODE_ENV”: “development” // 开发环境 }, “verbose”: true, “ext”: “js” // js文件}更改package.json中的scripts"dev:server": “nodemon server/server.js”,这样dev:server就可以监听到文件的变化,自动重启了。本小结改动位于仓库的2-10分支 ...

April 4, 2019 · 1 min · jiezi

6.eslint和editorconfig配置

本章节内容主要时要时参照官方文档配置即可。eslint配置在根项目目录项新建.eslintrc文件// 这里要安装 eslint-config-standard包,安装完后按照提示,安装相关的依赖。// 这里主要时对项目中所有内容生效,要求比较低{ “extends”: “standard”}然后在client目录下新建同样的文件,来规范client端的代码// babel-eslint , eslint-config-airbnb及其相关依赖包{ “parser”: “babel-eslint”, “env”: { “browser”: true, “es6”: true, “node”: true }, “parserOptions”: { “ecmaVersion”: 6, “sourceType”: “module” }, “extends”: “airbnb”, “rules”: { “semi”: [0] }}在webpack客户端和服务端的配置文件中,在rules下新增一个rule。 { enforce: ‘pre’, // 在babel编译之前进行检查 test: /.(js|jsx)$/, loader: ’eslint-loader’, // 使用eslint-loader,需安装 exclude: [ resolvePath(’../node_modules’) ] },配置完这些后,我们启动我们的服务。会发现出现很多错误,window环境下可以会见到很多"LF"的错误,这是因为不同的操作系统,行末的符号时不一致的。所以我们需要配置editorconfig文件。现在主流的ide,如webstorm,vs code都带有edit的插件,在项目根目录下新建.editorconfig文件,按照如下配置即可。root = true // 是否为根节点,说明在子目录下也可配置该文件[*] // 用于所有文件charset = utf-8 //编码格式indent_style = space //缩进样式indent_size = 2 // 缩进大小end_of_line = lf // 以lf结尾insert_final_newline = true // 自动在文件末尾插入新行trim_trailing_whitespace = true // 去除行末的空格git hook在提交代码之前进行lint检查,如果不合格,不能提交代码。以前一直用的是husky -哈士奇,后来在vue-cli中看到了yorkie,看说明应该是husky的改进版本。下面来说说两者的配置方式。 // package.json的scripts增加lint命令,检查client目录下的代码 “lint”: “eslint –ext .js –ext .jsx client/” // husky:在scripts下配置 “precommit”: “npm run lint” // yorkie, 与scripts平级 “gitHooks”: { “pre-commit”: “npm run lint” }这样,在你commit代码前就会进行检查,不符合要求的代码不能提交。本节的配置位于仓库的2-9分支 ...

April 4, 2019 · 1 min · jiezi

记一次vue项目webpack升级

遇到的坑webpack一定要锁版本,这东西的版本感觉以一个大坑,说不定这个版本支持,下个版本就不支持了,只能是在issue里看到别人提pr,然后说下个版本修复,尴尬的要死MiniCssExtractPlugin得放在vue-style-loader后面如果是ssr项目,babel升级的影响会比较大,其他都可以升级,babel要慎重涉及依赖升级webpackwebpack-cliwebpack-dev-servermini-css-extract-pluginhtml-webpack-pluginbabel (且升且珍惜)步骤添加mode,区分开发和生产,开发编译速度快,生产打包体积小删除extract-text-webpack-plugin(webpack4不推荐),添加mini-css-extract-plugin依赖或升级extract-text-webpack-plugin版本if(process.env.NODE_ENV === ‘production’) { return [‘vue-style-loader’, MiniCssExtractPlugin.loader ].concat(loaders);}return [‘vue-style-loader’].concat(loaders)3.删除CommonsChunkPlugin(webpack4弃用),添加splitChunksoptimization: { minimize: false, splitChunks: { cacheGroups: { vendor: { chunks: “all”, test: /[\/]node_modules[\/]/, name: “vendor”, minChunks: 1, maxInitialRequests: 5, minSize: 0, priority: 98 } } }},至此,旧项目升级就完成了

April 4, 2019 · 1 min · jiezi

5.开发时服务端渲染

由于配置了webpack-dev-server,客户端启动时,就不必再本地生成dist目录。但是服务器端的编译还是需要本地的dist目录,所以本节我们将会配置服务端的内容,使得服务端也不用依赖本地的dist目录。相关依赖npm i axios // http依赖,估计大家都知道npm i memery-fs -D // 相关接口和node的fs一样,只不过是在内存中生成文件npm i http-proxy-middleware -D // 服务器端一个代理的中间件本章节内容比较难,对于没有接触过node和webpack的同学,理解起来不是那么容易。我也是不知道看了多少遍,才大概知道其流程。开发时的服务端配置以前server.js中要依赖本地dist中的文件,所以首先要对其进行更改。const static = require(’./util/dev-static’)const isDev = process.env.NODE_ENV === ‘development’ // 增加环境的判断const app =express()if(!isDev) { // 生产环境,和以前的处理方式一样 const serverEntry = require(’../dist/server-entry’).default // 配置静态文件目录 app.use(’/public’, express.static(path.join(__dirname, ‘../dist’))) const template = fs.readFileSync(path.join(__dirname, ‘../dist/index.html’), ‘utf-8’) // https://blog.csdn.net/qq_41648452/article/details/80630598 app.get(’’, function(req, res) { const appString = ReactSSR.renderToString(serverEntry) res.send(template.replace(’<!– <app /> –>’, appString)) })} else { // 开发环境,进行单独的处理 static(app)}开发环境中的static方法,位于server/util/dev-static.js中,接受一个app参数。按照生产模式的处理逻辑,开发模式下的配置也分为如下几点:获取打包好的入口文件,即server-entry.js文件获取模板文件将模板文件中的内容替换为server-entry.js中的内容,返回给客户端对静态文件的请求进行处理。获取模板文件获取模板文件最简单,所以最先解决这个问题。配置客户端的devServer时,再http://localhost:8888下面就可以访问到index.html文件,调用下面getTemplate方法就可以拿到模板文件。const axios = require(‘axios’)const getTemplate = () => { return new Promise((resolve, reject) => { axios.get(‘http://localhost:8888/public/index.html’) .then(res => { resolve(res.data) }) .catch(reject) })}获取server-entry.js文件获取服务端的文件,我们需要用到memory-fs包,直接再内存中生成打包好的文件,读取速度更快,那要怎么配置呢?const path = require(‘path’)const webpack = require(‘webpack’)const MemoryFs = require(‘memory-fs’)const serverConfig = require(’../../build/webpack.config.server’) // 读取配置文件// webpack(serverConfig)和我们的build:server命令类似const serverCompile = webpack(serverConfig) // webpack处理const mfs = new MemoryFs()serverCompile.outputFileSystem = mfs // 将文件的输出交给mfs;默认应该是node的fs模块// 监听文件的变化serverCompile.watch({}, (err, stats) => { if(err) throw err // stats对象有一些状态信息,如我们编译过程中的一些错误或警告,我们直接将这些信息打印出来 stats = stats.toJson() stats.errors.forEach(err => console.err(err)) stats.warnings.forEach(warn => console.warn(warn)) // 通过配置文件获取文件的路径和名称 const bundlePath = path.join( serverConfig.output.path, serverConfig.output.filename ) // 读取文件的内容 const bundle = mfs.readFileSync(bundlePath, ‘utf-8’)})所以服务端文件也获取到了?其实还是有问题的,我们获取的仅仅是字符串,并不是node中的一个模块(如果听不懂,先去补补node中模块的概念),所以还需要做进一步的处理。const Module = module.constructor// node中,每个文件中都有一个Module变量,不懂的就要多学习了const m = new Module() m._compile(bundle, serverConfig.output.filename) // 将字符串编译为一个模块serverBundle = m.exports.default // 这才是我们需要的内容替换内容app.get(’’, function (req, res) { getTemplate().then(template => { const content = ReactDomSSR.renderToString(serverBundle) res.send(template.replace(’<!– <app /> –>’, content)) }) })静态资源处理和模板文件一样,静态资源我们将会代理到localhost:8888里面去获取 const proxy = require(‘http-proxy-middleware’) app.use(’/public’, proxy({ target: ‘http://localhost:8888’ }))到这里,开发时服务端渲染就完成了。本小节完整代码位于仓库的2-8分支,觉得有用的可以去start一下。 ...

April 3, 2019 · 1 min · jiezi

4.hot-load-replacement配置(react-hot-loaderV4)

什么是热更新按照前面的配置,更改App.jsx中的内容,保存后,页面上的内容也会实时的变化,这难道不是热更行吗?我刚开始也有这样的疑问。但是,你要注意,目前更改内容保存后,浏览器执行的是刷新操作,相当于F5刷新页面。而热更新就像ajax一样,只会更改修改的那部分,不会引起浏览器的刷新。如何配置热更新热更新主要用到的包时react-hot-loader,课程中用的包版本较低,配置比较麻烦(相对于新版本),我在网上搜了一下react-hot-loader的配置,画风基本时这样的。目前react-hot-loader版本为4.8.2版本,根据官网的介绍,现在react-hot-loader可以直接当作正常的依赖,可以不用当作开发依赖。打包时,会自动去掉这个包。根据官网的说明,配置起来也很简单。配置.babelrc文件{ “plugins”: [“react-hot-loader/babel”]}将App.jsx导出的App用hot包裹import { hot } from ‘react-hot-loader’class App extends React.Component { render() { return ( <div>This is app</div> ) }}export default hot(App)同时,client的webpack配置中devServer的hot属性设置为true即可每次启动时,在浏览器的console里面就会出现 [WDS] Hot Module Replacement enabled. 的提示,代表热更新成功。另外,还有一个注意事项。配置热更新成功后,更改文件内容,在network窗口会发现有新的请求文件,本人浏览器窗口会去请求热更新的文件,请求地址如下http://localhost:8888/publica31180d047a509a4bdc0.hot-update.jsonpublic后面缺少’/’,奇怪的是并没有报404的错误,还是可以请求得到。但是最好还是处理一下,将webpack配置中的publicPath属性值改为’/public/’,以免以后出现问题。相关代码位于仓库的2-7分支

April 2, 2019 · 1 min · jiezi

3.webpack-dev-server配置

修复前面版本的一些问题在前面2-5分支中,运行后控制台总会出现一些错误。原因就是client目录下app.js和App.jsx的文件名相似引起的。因此我们将app.js重新命名为main.js,然后修改客户端webpack的入口文件为main.js即可。webpack-dev-server的作用前面都是buil命令,直接在硬盘上生成打包好的文件。而我们在开发过程中,往往会在本地启动一个服务器,webpack-dev-server就是帮助我们启动一个本地的服务器。本届主要时配置webpack的devServer属性,感兴趣的可以先去看看官方文档。本节内容需要安装两个开发环境的依赖。webpack-dev-server 启动本地服务器cross-env 判断不同系统下的开发或生产环境由于开发时的配置,所以主要是修改client端的配置文件。而且需要判断是否为开发环境。const isDev = process.env.NODE_ENV === ‘development’ //判断是否为开发环境// 以前是直接 module.exports = config {}// 现在需要在开发时增加一些配置config = {….} // 还是以前的配置,省略// 如果时开发环境,增加如下配置if (isDev) { config.devServer = { host: ‘0.0.0.0’, // 可以通过localhost或127.0.0.1方式访问 port: ‘8888’, // 端口号 contentBase: path.join(__dirname, ‘../dist’), // 访问的文件目录 // hot: true, // 热更替,后面配置react后会开启 overlay: { errors: true // 在浏览器窗口出口错误的提示层 }, publicPath: ‘/public’, // 与前面的功能一致 historyApiFallback: { index: ‘/public/index.html’ // 404页面默认回到首页 } }}module.exports = config前面我们在webpack中配置了mode:‘development’,就已经设置为开发模式了。关于mode这个属性,可以去看看官方文档。接下来,我们在package.json中配置scripts。// cross-env判断不同系统环境下的NODE_ENV的值 “dev:client”: “cross-env NODE_ENV=development webpack-dev-server –config build/webpack.config.client.js"注意,运行dev:client命令时,记得先删除本地编译的dist目录。本节代码位于仓库的2-6分支 ...

April 2, 2019 · 1 min · jiezi

【webpack4】用不同语言语法编写webpack配置文件

写在前面webpack配置文件默认情况下为webpack.config.js,即用原生js语法书写的配置文件。然而,在我们的项目中,有时候是使用的是其他语言语法进行编程,例如:es6、coffeeScript、typeScript等语言语法。为此,webpack为我们提供了可以采用不同语言语法类型书写配置文件的功能。具体可以支持的文件拓展可以看这里:https://github.com/gulpjs/int…可以看到,webpack为我们提供了丰富多样可供选择的文件拓展。下面介绍一下最常见的webpack配置文件类型:TypeScript1、安装依赖如果想要使用TypeScript来书写webpack配置文件,首先要先安装依赖:npm install –save-dev typescript ts-node @types/node @types/webpack如果需要用到webpack-dev-server,还需要安装:npm install –save-dev @types/webpack-dev-server2、编写webpack配置文件(1)把webpack配置文件的文件名改为:webpack.config.tsTypeScript的文件拓展名为.ts,所以我们需要同时把webpack配置文件的文件名改为.ts拓展名(原来默认为webpack.config.js)当我们把webpack配置文件名拓展改为.ts时,webpack也会自动读取该拓展名下的文件。即不需要这样设置:>> webpack –config webpack.config.tswebpack会自动帮我们读取webpack.config.ts文件,不需要我们再去设置了(2)编写webpack.config.ts配置文件利用TypeScript编写webpack配置文件时,webpack配置文件的结构同以前一样,只不过语言变为Typescript而已。//webpack.config.tsimport path from ‘path’import webpack from ‘webpack’?const config: webpack.Configuration = { entry: ‘./src/index.js’, output: { filename: ‘bundle.js’, path: path.resolve(__dirname, ‘dist’), publicPath: path.resolve(__dirname, ‘dist’), },}export default config在本webpack配置文件webpack.config.ts中,我们把require语法改为ts中的import、export静态模块引入导出的语法,以便我们测试。3、编写TypeScript配置文件通常来说,大多情况下,我们使用某种语法、插件等,它都会有自己一份默认的配置,在比较简单的项目中,毋需我们编写配置文件。但是,在利用TypeScript书写webpack配置文件时,我们还需要额外编写TypeScript配置文件tsconfig.json:{ “compilerOptions”: { /* ts-node只支持commonjs规范 */ “module”: “commonjs”, “target”: “es5”, “esModuleInterop”: true, }}这是因为我们在前面安装的依赖ts-node不支持commonjs之外的模块语法,所以我们必须在TypeScript的配置文件tsconfig.json配置compilerOptions中module字段为:commonjs,否则,webpack会报错。ps:在安装依赖的时候,我们也需要考虑依赖的局限性。比如某些依赖依赖于其他的依赖,在项目开发的时候,我们需要把其涉及到的其他依赖一同安装。另外,依赖不是万能的,在确定安装依赖的时候,需要额外去学习该依赖相关知识。coffeeScript1、安装依赖与上面的内容相似,首先我们需要安装相关依赖:npm install –save-dev coffee-script2、编写webpack配置文件(1)把webpack配置文件的文件名改为:webpack.config.coffeeCoffeeScript的文件拓展名为.coffee,所以我们需要同时把webpack配置文件的文件名改为.coffee拓展名(原来默认为webpack.config.js)当我们把webpack配置文件名拓展改为.coffee时,webpack也会自动读取该拓展名下的文件。即不需要这样设置:webpack –config webpack.config.coffeewebpack会自动帮我们读取webpack.config.coffee文件,不需要我们再去设置(2)利用coffeeScript重新编写webpack.config.coffee文件//webpack.config.coffeewebpack = require(‘webpack’)path = require(‘path’)config = mode: ‘production’ entry: ‘./src/index.js’ output: path: path.resolve(__dirname, ‘dist’) filename: ‘bundle.js’module.exports = config用coffeeScript编写webpack配置文件时,毋需向TypeScript一样编写ts配置文件,因为coffeeScript安装的依赖没有其他的兼容性问题出现。ES6利用es6写webpack配置文件的原理同上面一样,都是把其他类型的语言语法编译成原生js。把es6编译成原生js可以使用babel进行编译(也有其他选择)。1、安装依赖npm install –save-dev babel-core babel-loader babel-preset-es2015 babel-preset-stage-3其中,要使用babel编译器,我们需要安装的依赖有babel-core。babel-core包中囊括了babel的核心方法。2、编写webpack配置文件由于es6语法写的文件名拓展也是.js,那么webpack如何识别该js文件,并把它交予babel进行编译呢?(1)webpack.config.[loader].js把webpack配置文件的文件名改为webpack.config.babel.js,其中babel字段表示需要优先使用babel-loader对该webpack配置文件进行编译。同样地,我们可以把webpack.config.[loader].js中的[loader]替换成我们需要的loader名。这也是我们需要安装babel-loader的原因。(2)编写webpack.config.babel.js为了体现es6语法,我们把webpack配置文件改写成:import path from ‘path’// example of an imported pluginconst CustomPlugin = config => ({ …config, name: ‘custom-plugin’});?export default { entry: ‘./src/index.js’, output: { filename: ‘bundle.js’, path: path.resolve(__dirname, ‘dist’) },}其中,import、export、… 语法为es6语法3、编写babel配置文件.babelrcbabel实质是一个支持众多语法编译转化的编译器,为了保证babel的可拓展性,作者把babel设计成可以灵活配置,支持安装插件的模式。因此,我们需要自行配置babel,使之支持es6编译。{ “presets”: [ “es2015”, “stage-3”],}其中,我们需要安装babel-preset-es2015的包,使得babel支持es6编译。另外,使用…config语法需要安装babel-preset-stage-3包,否则会编译错误。总之,我们可以使用各种各样的语言语法来编写webpack配置文件,它们的原理都是使用对应的编译器编译成原生的js。所以我们在编程的时候,都需要安装编译器的相关依赖,并且在必要的时候,还需要对编译器进行配置。 ...

April 2, 2019 · 1 min · jiezi

2.React服务端渲染基础配置

服务端渲染服务端渲染(SSR)主要是为了SEO,加快首屏的加载速度等作用。利用react-dom/server提供的工具,我们很容易进行服务端渲染。基本原理服务端渲染的基本原理就是读取我们的模板文件,然后将其中的内容替换成我们自己的代码,然后生成一个完整的html文件返回给前端页面。webpack配置在第一篇文章中,已经进行了基础的配置,本文是在前面的基础上来配置的。本次配置需要安装以下两个依赖express, 涉及到服务端代码,用到express包rimraf, 看着名字就知道是删库跑路的包。每次我们运行build命令时,都会生成新的文件。我们可以用这个包先删除dist目录,然后在重新生成新的dist目录。首先在client目录下新增template.html和server-entry.js两个文件。前面的html时模板文件,后面的js作为服务端的入口文件。// template文件很简单,只有一个id为app的div,后面我们将会把<!– <app /> –>替换为我们自己的内容。<!DOCTYPE html><html lang=“en”><head> <meta charset=“UTF-8”> <meta name=“viewport” content=“width=device-width, initial-scale=1.0”> <meta http-equiv=“X-UA-Compatible” content=“ie=edge”> <title>Document</title></head><body> <div id=“app”><!– <app /> –></div></body></html>// 入口文件目前也很简单,只是导入App组件import React from ‘react’import App from ‘./App.jsx’export default <App />在build目录下新增webpack.config.server.js文件,作为服务端打包的配置文件。同时为了区分客户端,将客户端的配置文件改为webpack.config.client.js。服务端与客户端的配置基本一样,主要时入口文件和出口文件的配置不同。 entry: { app: resolvePath(’../client/server-entry.js’) // 服务端入口文件 }, output: { filename: ‘server-entry.js’, // 输出文件名 path: resolvePath(’../dist’), // 输出路劲 publicPath: ‘’, // libraryTarget: ‘commonjs2’ // 模块化的方式 },在项目目录下新建server文件夹,新建一个server.js文件,该文件主要为服务端逻辑。const express = require(’express’)const ReactSSR = require(‘react-dom/server’)const fs = require(‘fs’)const path = require(‘path’)const serverEntry = require(’../dist/server-entry’).default // 打包好的服务端文件const app =express()const template = fs.readFileSync(path.join(__dirname, ‘../dist/index.html’), ‘utf-8’) // 读取模板文件app.get(’*’, function(req, res) { const appString = ReactSSR.renderToString(serverEntry) res.send(template.replace(’<!– <app /> –>’, appString)) // 将模板文件中的注释替换为我们自己的内容,然后返回到客户端})app.listen(3333, function() { console.log(‘server is listen on 3333’)})服务端渲染的基本逻辑就已经完成。接下来我们在package.json文件中新增一些命令。 “scripts”: { “build:client”: “webpack –config build/webpack.config.client.js”, // 编译客户端代码 “build:server”: “webpack –config build/webpack.config.server.js”, // 编译服务端代码 “clear”: “rimraf dist”, // 每次build前,先自动删除dist目录 “build”: “npm run clear && npm run build:client && npm run build:server”,// build客户端和服务端的代码 “start”: “node server/server.js” // 启动服务器 },先运行build命令,然后运行start命令,访问localhost:3333,就可以看到内容了。而且在network窗口中可以看到返回的时完整的html页面,而不是一个空页面。但时目前请求js文件,返回的也是html文件。因为在server.js中,任意的请求都是返回html文件。可以通过express来配置静态文件目录。// 以/public开头的请求都会去dist目录中找。app.use(’/public’, express.static(path.join(__dirname, ‘../dist’)))同时需要修改客户端和服务端的webpack配置。 // 会在路径前加上/public前缀 output: { publicPath: ‘/public’, },重新运行build和start命令,访问3333端口,就会返现请求都是正常的。从返回的html文件中,script标签的src属性中的路径会带有/public前缀,这就是publicPath属性的作用。至此,服务端渲染的基础配置就已经完成。本次的代码位于仓库的2-5分支。在使用rimraf时,window可能会遇到一些权限相关的问题,可能的解决方法点这里 ...

April 2, 2019 · 1 min · jiezi

1.webpack基础配置与loader的基础应用

写在开头该系列文章主要是本人在学习慕课网React全栈课程中的一些记录和分享。该课程主要是利用React构建cnode网站,接口由cnode官方提供。由于课程中的webpack,babel版本较老,本次分享均是用的webpack4和Babel7。本系列文章重点不是React,主要是分享前端工程化的构建和服务端渲染(SSR)。本次分享的代码将会放到我的github上面。工程初始化和webpack基础配置新建项目文件夹,在cmd窗口中运行npm init,输入一些配置项后即可生成一个npm项目。运行git init,对该项目进行git版本管理。在项目中新建build和client文件夹,build文件夹存放webpack配置文件,client文件夹存放客户端的开发文件。首先安装基础的依赖React和Webpack。npm i react -S npm i webpack -D // -D为开发依赖npm i webpack-cli -D // webpack4,需要安装cli依赖新建一些文件build/webpack.config.js // webpack配置文件client/app.js // 项目的入口文件client/App.jsx // react入口文件webpack基础配置,详细配置可参照官网const path = require(‘path’) // path包解决不同操作系统中路径不一致问题function resolvePath(filePath) { return path.join(__dirname, filePath);}module.exports = { mode: ‘development’, // 开发模式或生产模式 // 入口文件,webpack编译的入口 entry: { app: resolvePath(’../client/app.js’) }, // 打包后文件的输出地址 output: { filename: ‘[name].[hash].js’, //name和hash是其中的两个变量 path: resolvePath(’../dist’), // 打包后文件的位置 publicPath: ’’ // }}webpack基本配置完成了,在package.json中的scripts中增加一个build命令 “scripts”: { “build”: “webpack –config build/webpack.config.js” },在app.js中随便写一点内容,在cmd中运行npm run build,在当前文件夹下会生成一个dist目录,该目录下即为经webpack编译后的文件。该部分代码位于仓库的2-3分支下如果初始化git后,在项目下添加 .gitignore 文件,用来配置不需要版本管理的文件夹或文件,如node_modules等babel-loader及babel的配置由于在项目中用到ES6和jsx语法,所有需要用babel先编译。babel-loader也是react官方的编译器。我们现在app和App文件中写一些简单的内容。App.jsx import React from ‘react’ // 一个简单的react组件 export default class App extends React.Component { render() { return ( <div>This is app</div> ) } }app.js //将App.jsx中的组件挂载到body上(仅作演示,不建议挂载到body上) import React from ‘react’ import ReactDOM from ‘react-dom’ import App from ‘./App’ ReactDOM.render(<App />, document.body) 配置webpack中的loader,loader主要是转换代码的作用,如将jsx代码转为js代码。我们需要安装babel-loader,@babel/core和@babel/preset-react三个依赖,均用-D安装。 resolve: { extensions: [’.js’,’.jsx’] // 默认文件后缀。在app.js中,直接引入App,而不是App.jsx。所有的js和jsx文件在引入时均可省略后缀 }, // 配置loader module: { rules: [ { test: /.jsx$/, // 正则,处理以.jsx结尾的文件 loader: ‘babel-loader’ // 使用的loader }, { test: /js$/, // 主要是将ES6或更高级别的无法转为ES5版本 loader: ‘babel-loader’, exclude: [ resolvePath(’../node_modules’) // 忽略node_modules中的文件 ] } ] },配置完webpack后,babel还没有生效。需要在项目中新建一个.babelrc文件,配置项如下。// babel7的配置比较简洁,直接使用官方的preset-react即可。{ “presets”: ["@babel/preset-react"]}运行npm run build命令,新生成的js文件就会包含react相关的代码了。目前生成的文件均为js文件,并没有html文件的生成。我们之需要安装html-webpack-plugin,然后再webpack中配置即可const HTMLPlugin = require(‘html-webpack-plugin’)modeule.exports = { //……… plugins: [ new HTMLPlugin() ]}再次运行build命令后,会再dist目录下生成一个index.html文件,打开即可看见我们再App.jsx中的内容。该部分代码位于仓库的2-4分支下 ...

March 31, 2019 · 1 min · jiezi

史上最清晰易懂的babel配置解析

标题党了哈哈哈~~~原文地址相信很多人和笔者从前一样,babel的配置都是从网上复制黏贴或者使用现成的脚手架,虽然这能够工作但还是希望大家能够知其所以然,因此本文将对babel(babel@7)的配置做一次较为完整的梳理。语法和apies6增加的内容可以分为语法和api两部分,搞清楚这点很重要,新语法比如箭头函数、解构等:const fn = () => {}const arr2 = […arr1]新的api比如Map、Promise等:const m = new Map()const p = new Promise(() => {})@babel/core@babel/core,看名字就知道这是babel的核心,没他不行,所以首先安装这个包npm install @babel/core它的作用就是根据我们的配置文件转换代码,配置文件通常为.babelrc(静态文件)或者babel.config.js(可编程),这里以.babelrc为例,在项目的根目录下创建一个空文件命名为.babelrc,然后创建一个js文件(test.js)测试用:/* test.js /const fn = () => {}这里我们安装下@babel/cli以便能够在命令行使用babelnpm install @babel/cli安装完成后执行babel编译,命令行输入npx babel test.js –watch –out-file test-compiled.js结果发现test-compiled.js的内容依然是es6的箭头函数,不用着急,我们的.babelrc还没有写配置呢Plugins和PresetsNow, out of the box Babel doesn’t do anything. It basically acts like const babel = code => code; by parsing the code and then generating the same code back out again. You will need to add plugins for Babel to do anything.上面是babel官网的一段话,可以理解为babel是基于插件架构的,假如你什么插件也不提供,那么babel什么也不会做,即你输入什么输出的依然是什么。那么我们现在想要把剪头函数转换为es5函数只需要提供一个箭头函数插件就可以了:/ .babelrc /{ “plugins”: ["@babel/plugin-transform-arrow-functions"] }转换后的test-compiled.js为:/ test.js /const fn = () => {}/ test-compiled.js /const fn = function () {}那我想使用es6的解构语法怎么办?很简单,添加解构插件就行了:/ .babelrc /{ “plugins”: [ “@babel/plugin-transform-arrow-functions”, “@babel/plugin-transform-destructuring” ] }问题是有那么多的语法需要转换,一个个的添加插件也太麻烦了,幸好babel提供了presets,他可以理解为插件的集合,省去了我们一个个引入插件的麻烦,官方提供了很多presets,比如preset-env(处理es6+规范语法的插件集合)、preset-stage(处理尚处在提案语法的插件集合)、preset-react(处理react语法的插件集合)等,这里我们主要介绍下preset-env:/ .babelrc /{ “presets”: ["@babel/preset-env"] }preset-env@babel/preset-env is a smart preset that allows you to use the latest JavaScript without needing to micromanage which syntax transforms (and optionally, browser polyfills) are needed by your target environment(s).以上是babel官网对preset-env的介绍,大致意思是说preset-env可以让你使用es6的语法去写代码,并且只转换需要转换的代码。默认情况下preset-env什么都不需要配置,此时他转换所有es6+的代码,然而我们可以提供一个targets配置项指定运行环境:/ .babelrc /{ “presets”: [ ["@babel/preset-env", { “targets”: “ie >= 8” }] ] }此时只有ie8以上版本浏览器不支持的语法才会被转换,查看我们的test-compiled.js文件发现一切都很好:/ test.js /const fn = () => {}const arr1 = [1, 2, 3]const arr2 = […arr1]/ test-compiled.js /var fn = function fn() {};var arr1 = [1, 2, 3];var arr2 = [].concat(arr1);@babel/polyfill现在我们稍微改一下test.js:/ test.js /const fn = () => {}new Promise(() => {})/ test-compiled.js /var fn = function fn() {};new Promise(function () {});我们发现Promise并没有被转换,什么!ie8还支持Promise?那是不可能的…。还记得本文开头提到es6+规范增加的内容包括新的语法和新的api,新增的语法是可以用babel来transform的,但是新的api只能被polyfill,因此需要我们安装@babel/polyfill,再简单的修改下test.js如下:/ test.js /import ‘@babel/polyfill’const fn = () => {}new Promise(() => {})/ test-compiled.js /import ‘@babel/polyfill’;var fn = function fn() {};new Promise(function () {});现在代码可以完美的运行在ie8的环境了,但是还存在一个问题:@babel/polyfill这个包的体积太大了,我们只需要Promise就够了,假如能够按需polyfill就好了。真巧,preset-env刚好提供了这个功能:/ .babelrc /{ “presets”: [ ["@babel/preset-env", { “modules”: false, “useBuiltIns”: “entry”, “targets”: “ie >= 8” }] ] }我们只需给preset-env添加一个useBuiltIns配置项即可,值可以是entry和usage,假如是entry,会在入口处把所有ie8以上浏览器不支持api的polyfill引入进来,如下:/ test.js /import ‘@babel/polyfill’const fn = () => {}new Promise(() => {})/ test-compiled.js /import “core-js/modules/es6.array.copy-within”;import “core-js/modules/es6.array.every”;import “core-js/modules/es6.array.fill”;… //省略若干引入import “core-js/modules/web.immediate”;import “core-js/modules/web.dom.iterable”;import “regenerator-runtime/runtime”;var fn = function fn() {};new Promise(function () {});细心的你会发现transform后,import ‘@babel/polyfill’消失了,反倒是多了一堆import ‘core-js/…‘的内容,事实上,@babel/polyfill这个包本身是没有内容的,它依赖于core-js和regenerator-runtime这两个包,这两个包提供了es6+规范的运行时环境。因此当我们不需要按需polyfill时直接引入@babel-polyfill就行了,它会把core-js和regenerator-runtime全部导入,当我们需要按需polyfill时只需配置下useBuiltIns就行了,它会根据目标环境自动按需引入core-js和regenerator-runtime。前面还提到useBuiltIns的值还可以是usage,其功能更为强大,它会扫描你的代码,只有你的代码用到了哪个新的api,它才会引入相应的polyfill:/ .babelrc /{ “presets”: [ ["@babel/preset-env", { “modules”: false, “useBuiltIns”: “usage”, “targets”: “ie >= 8” }] ] }transform后的test-compiled.js相应的会简化很多:/ test.js /const fn = () => {}new Promise(() => {})/ test-compiled.js /import “core-js/modules/es6.promise”;import “core-js/modules/es6.object.to-string”;var fn = function fn() {};new Promise(function () {});遗憾的是这个功能还处于试验状态,谨慎使用。 事实上假如你是在写一个app的话,以上关于babel的配置差不多已经够了,你可能需要添加一些特定用途的Plugin和Preset,比如react项目你需要在presets添加@babel/preset-react,假如你想使用动态导入功能你需要在plugins添加@babel/plugin-syntax-dynamic-import等等,这些不在赘述。假如你是在写一个公共的库或者框架,下面提到的点可能还需要你注意下。@babel/runtime有时候语法的转换相对复杂,可能需要一些helper函数,如转换es6的class:/ test.js /class Test {}/ test-compiled.js /function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError(“Cannot call a class as a function”); } }var Test = function Test() { _classCallCheck(this, Test);};示例中es6的class需要一个_classCallCheck辅助函数,试想假如我们多个文件中都用到了es6的class,那么每个文件都需要定义一遍_classCallCheck函数,这也是一笔不小的浪费,假如将这些helper函数抽离到一个包中,由所有的文件共同引用则可以减少可观的代码量。而@babel/runtime做的正好是这件事,它提供了各种各样的helper函数,但是我们如何知道该引入哪一个helper函数呢?总不能自己手动引入吧,事实上babel提供了一个@babel/plugin-transform-runtime插件帮我们自动引入helper。我们首先安装@babel/runtime和@babel/plugin-transform-runtime:npm install @babel/runtime @babel/plugin-transform-runtime然后修改babel配置如下:/ .babelrc /{ “presets”: [ ["@babel/preset-env", { “modules”: false, “useBuiltIns”: “usage”, “targets”: “ie >= 8” }] ], “plugins”: [ “@babel/plugin-transform-runtime” ] }现在我们再来看test-compiled.js文件,里面的_classCallCheck辅助函数已经是从@babel/runtime引入的了:/ test.js /class Test {}/ test-compiled.js /import _classCallCheck from “@babel/runtime/helpers/classCallCheck”;var Test = function Test() { _classCallCheck(this, Test);};看到这里你可能会说,这不扯淡嘛!几个helper函数能为我减少多少体积,我才懒得安装插件。事实上@babel/plugin-transform-runtime还有一个更重要的功能,它可以为你的代码创建一个sandboxed environment(沙箱环境),这在你编写一些类库等公共代码的时候尤其重要。上文我们提到,对于Promise、Map等这些es6+规范的api我们是通过提供polyfill兼容低版本浏览器的,这样做会有一个副作用就是污染了全局变量,假如你是在写一个app还好,但如果你是在写一个公共的类库可能会导致一些问题,你的类库可能会把一些全局的api覆盖掉。幸好@babel/plugin-transform-runtime给我们提供了一个配置项corejs,它可以将这些变量隔离在局部作用域中:/ .babelrc /{ “presets”: [ ["@babel/preset-env", { “modules”: false, “targets”: “ie >= 8” }] ], “plugins”: [ ["@babel/plugin-transform-runtime", { “corejs”: 2 }] ] }注意:这里一定要配置corejs,同时安装@babel/runtime-corejs2,不配置的情况下@babel/plugin-transform-runtime默认是不引入这些polyfill的helper的。corejs的值现阶段一般指定为2,可以近似理解为是@babel/runtime的版本。我们现在再来看下test-compiled.js被转换成了什么:/ test.js /class Test {}new Promise(() => {})/ test-compiled.js /import _Promise from “@babel/runtime-corejs2/core-js/promise”;import _classCallCheck from “@babel/runtime-corejs2/helpers/classCallCheck”;var Test = function Test() { _classCallCheck(this, Test);};new _Promise(function () {});如我们所愿,已经为Promise的polyfill创建了一个沙箱环境。最后我们再为test.js稍微添加点内容:/ test.js /class Test {}new Promise(() => {})const b = [1, 2, 3].includes(1)/ test-compiled.js */import _Promise from “@babel/runtime-corejs2/core-js/promise”;import _classCallCheck from “@babel/runtime-corejs2/helpers/classCallCheck”;var Test = function Test() { _classCallCheck(this, Test);};new _Promise(function () {});var b = [1, 2, 3].includes(1);可以发现,includes方法并没有引入辅助函数,可这明明也是es6里面的api啊。这是因为includes是数组的实例方法,要想polyfill必须修改Array的原型,这样一来就污染了全局环境,因此@babel/plugin-transform-runtime是处理不了这些es6+规范的实例方法的。tips以上基本是本文的全部内容了,最后再来个总结和需要注意的地方:本文没有提到preset-stage,事实上babel@7已经不推荐使用它了,假如你需要使用尚在提案的语法,请直接添加相应的plugin。对于普通项目,可以直接使用preset-env配置polyfill对于类库项目,推荐使用@babel/runtime,需要注意一些实例方法的使用本文内容是基于babel@7,项目中遇到问题可以尝试更新下babel-loader的版本…待补充全文完 ...

March 31, 2019 · 3 min · jiezi

从零开始开发一个react脚手架(二)

上一篇已经初步整了个kkk-react,这一篇不写代码,粗略讲解下create-react-app的部分源码。前沿:科普下看源码的思路。以本人看过N多源码的经验总结,想要看这种脚手架或者npm包的源码,第一步就是看package.json的配置,一般看的就是main.js和script。main.js就是引入npm包后,取的真实的js文件地址。script就是脚手架命令,类似下面create-react-app “scripts”: { “start”: “react-scripts start”, “build”: “react-scripts build”, “test”: “react-scripts test”, “eject”: “react-scripts eject” },脚手架看script,npm包看main。找到script之后,就4个命令,第一个start就是开启本地服务,build就是打包文件,test没仔细看我估计就是代码检查吧,因为我们公司的test就是eslint检查,eject类似于生成配置文件之类的,因为他的配置走的是api,不是webpack配置文件,这个命令可能就是生成出对应的webpack文件(后面的两个没细看,不必太care)。弄清script之后,就去脚手架源代码里面找package.json。去这个文件里面看bin配置,说直接一点,为嘛script里面的命令能其效果呢,就是你安装一个包之后,如果这个包里面有bin配置,那么npm就会去node_modules里面的.bin目录下生成出对应的执行命令文件作为一个脚手架工具其实是可以分为两部分的。一是生成对应的dir和file,搭建好环境,让我们能直接跑起项目。 这一部分比较简单,我们到最后再来完成这一步(等我们完成自己的脚手架工具之后),类似create-create-app myApp之类的。二我感觉才是关键,是各种命令的实现,npm run start之类,接下来我会简单的解读下这一步的源码。create-react-app用的是分包管理lerna,这里就不讲了。直接找到react-scripts目录里面的package.json,可以看到虽然script里面有4条命令,但其实就是一个react-scripts命令,后面的只是参数。所有运行的react-scripts start|test|build,其实都是在执行react-scripts.js。看源码其实解析process.arg,然后解析出参数,最后执行对应的scripts目录下的文件,我们执行的是start,就是start.js文件。接下来就是解析这个start.js源码了。这里面有很多参数判定,代理处理,各种细节处理,抛开这些,核心其实就两个函数一 createCompiler,这个就是简单点就是 new webpack(config)的实例。因为平常我们写的大部分都是配置文件,实际是执行webpack打包的时候,他也就是读取配置文件,然后new webpack(config)。二 第二个就更简单了。读取各种配置参数,起一个服务,WebpackDevServer。平常我们都是通过命令行起一个服务,然后配置webpack.config.js里面的devServer,而现在就是通过API来实现。虽然没有讲的很细,但是明显可以发现,脚手架说白一点就是通过各种api来完成我们原本需要考命令行或者配置文件来做的事情。这样更加灵活,而且复用性高,起新项目,如果差别不大,几乎可以做到零配置,这样开发者压根就不需要关心业务之外的东西下一篇就开始真正写我们的自己的脚手架了。

March 31, 2019 · 1 min · jiezi

Vue + TypeScript + Element 项目实战及踩坑记

前言本文讲解如何在 Vue 项目中使用 TypeScript 来搭建并开发项目,并在此过程中踩过的坑 。 TypeScript 具有类型系统,且是 JavaScript 的超集,TypeScript 在 2018年 势头迅猛,可谓遍地开花。Vue3.0 将使用 TS 重写,重写后的 Vue3.0 将更好的支持 TS。2019 年 TypeScript 将会更加普及,能够熟练掌握 TS,并使用 TS 开发过项目,将更加成为前端开发者的优势。所以笔者就当然也要学这个必备技能,就以 边学边实践 的方式,做个博客项目来玩玩。此项目是基于 Vue 全家桶 + TypeScript + Element-UI 的技术栈,且已经开源,github 地址 blog-vue-typescript 。因为之前写了篇纯 Vue 项目搭建的相关文章 基于vue+mint-ui的mobile-h5的项目说明 ,有不少人加我微信,要源码来学习,但是这个是我司的项目,不能提供原码。所以做一个不是我司的项目,且又是 vue 相关的项目来练手并开源吧。1. 效果效果图:pc 端移动端完整效果请看:https://biaochenxuying.cn2. 功能已经完成功能[x] 登录[x] 注册[x] 文章列表[x] 文章归档[x] 标签[x] 关于[x] 点赞与评论[x] 留言[x] 历程[x] 文章详情(支持代码语法高亮)[x] 文章详情目录[x] 移动端适配[x] github 授权登录待优化或者实现[ ] 使用 vuex-class[ ] 更多 TypeScript 的优化技巧[ ] 服务器渲染 SSR3. 前端主要技术所有技术都是当前最新的。vue: ^2.6.6typescript : ^3.2.1element-ui: 2.6.3vue-router : ^3.0.1webpack: 4.28.4vuex: ^3.0.1axios:0.18.0redux: 4.0.0highlight.js: 9.15.6marked:0.6.14. 5 分钟上手 TypeScript如果没有一点点基础,可能没学过 TypeScript 的读者会看不懂往下的内容,所以先学点基础。TypeScript 的静态类型检查是个好东西,可以避免很多不必要的错误, 不用在调试或者项目上线的时候才发现问题 。类型注解TypeScript 里的类型注解是一种轻量级的为函数或变量添加约束的方式。变量定义时也要定义他的类型,比如常见的 :// 布尔值let isDone: boolean = false; // 相当于 js 的 let isDone = false;// 变量定义之后不可以随便变更它的类型isDone = true // 不报错isDone = “我要变为字符串” // 报错// 数字let decLiteral: number = 6; // 相当于 js 的 let decLiteral = 6;// 字符串let name: string = “bob”; // 相当于 js 的 let name = “bob”;// 数组 // 第一种,可以在元素类型后面接上 [],表示由此类型元素组成的一个数组:let list: number[] = [1, 2, 3]; // 相当于 js 的let list = [1, 2, 3];// 第二种方式是使用数组泛型,Array<元素类型>:let list: Array<number> = [1, 2, 3]; // 相当于 js 的let list = [1, 2, 3];// 在 TypeScript 中,我们使用接口(Interfaces)来定义 对象 的类型。interface Person { name: string; age: number;}let tom: Person = { name: ‘Tom’, age: 25};// 以上 对象 的代码相当于 let tom = { name: ‘Tom’, age: 25};// Any 可以随便变更类型 (当这个值可能来自于动态的内容,比如来自用户输入或第三方代码库)let notSure: any = 4;notSure = “我可以随便变更类型” // 不报错notSure = false; // 不报错// Void 当一个函数没有返回值时,你通常会见到其返回值类型是 voidfunction warnUser(): void { console.log(“This is my warning message”);}// 方法的参数也要定义类型,不知道就定义为 anyfunction fetch(url: string, id : number, params: any): void { console.log(“fetch”);}以上是最简单的一些知识点,更多知识请看 TypeScript 中文官网5. 5 分钟上手 Vue +TypeScriptvue-class-component vue-class-component 对 Vue 组件进行了一层封装,让 Vue 组件语法在结合了 TypeScript 语法之后更加扁平化:<template> <div> <input v-model=“msg”> <p>prop: {{propMessage}}</p> <p>msg: {{msg}}</p> <p>helloMsg: {{helloMsg}}</p> <p>computed msg: {{computedMsg}}</p> <button @click=“greet”>Greet</button> </div></template><script>import Vue from ‘vue’import Component from ‘vue-class-component’@Component({ props: { propMessage: String }})export default class App extends Vue { // initial data msg = 123 // use prop values for initial data helloMsg = ‘Hello, ’ + this.propMessage // lifecycle hook mounted () { this.greet() } // computed get computedMsg () { return ‘computed ’ + this.msg } // method greet () { alert(‘greeting: ’ + this.msg) }}</script>上面的代码跟下面的代码作用是一样的:<template> <div> <input v-model=“msg”> <p>prop: {{propMessage}}</p> <p>msg: {{msg}}</p> <p>helloMsg: {{helloMsg}}</p> <p>computed msg: {{computedMsg}}</p> <button @click=“greet”>Greet</button> </div></template><script>export default { // 属性 props: { propMessage: { type: String } }, data () { return { msg: 123, helloMsg: ‘Hello, ’ + this.propMessage } }, // 声明周期钩子 mounted () { this.greet() }, // 计算属性 computed: { computedMsg () { return ‘computed ’ + this.msg } }, // 方法 methods: { greet () { alert(‘greeting: ’ + this.msg) } },}</script>vue-property-decorator vue-property-decorator 是在 vue-class-component 上增强了更多的结合 Vue 特性的装饰器,新增了这 7 个装饰器:@Emit@Inject@Model@Prop@Provide@Watch@Component (从 vue-class-component 继承)在这里列举几个常用的@Prop/@Watch/@Component, 更多信息,详见官方文档import { Component, Emit, Inject, Model, Prop, Provide, Vue, Watch } from ‘vue-property-decorator’@Componentexport class MyComponent extends Vue { @Prop() propA: number = 1 @Prop({ default: ‘default value’ }) propB: string @Prop([String, Boolean]) propC: string | boolean @Prop({ type: null }) propD: any @Watch(‘child’) onChildChanged(val: string, oldVal: string) { }}上面的代码相当于:export default { props: { checked: Boolean, propA: Number, propB: { type: String, default: ‘default value’ }, propC: [String, Boolean], propD: { type: null } } methods: { onChildChanged(val, oldVal) { } }, watch: { ‘child’: { handler: ‘onChildChanged’, immediate: false, deep: false } }}vuex-classvuex-class :在 vue-class-component 写法中 绑定 vuex 。import Vue from ‘vue’import Component from ‘vue-class-component’import { State, Getter, Action, Mutation, namespace} from ‘vuex-class’const someModule = namespace(‘path/to/module’)@Componentexport class MyComp extends Vue { @State(‘foo’) stateFoo @State(state => state.bar) stateBar @Getter(‘foo’) getterFoo @Action(‘foo’) actionFoo @Mutation(‘foo’) mutationFoo @someModule.Getter(‘foo’) moduleGetterFoo // If the argument is omitted, use the property name // for each state/getter/action/mutation type @State foo @Getter bar @Action baz @Mutation qux created () { this.stateFoo // -> store.state.foo this.stateBar // -> store.state.bar this.getterFoo // -> store.getters.foo this.actionFoo({ value: true }) // -> store.dispatch(‘foo’, { value: true }) this.mutationFoo({ value: true }) // -> store.commit(‘foo’, { value: true }) this.moduleGetterFoo // -> store.getters[‘path/to/module/foo’] }}6. 用 vue-cli 搭建 项目笔者使用最新的 vue-cli 3 搭建项目,详细的教程,请看我之前写的 vue-cli3.x 新特性及踩坑记,里面已经有详细讲解 ,但文章里面的配置和此项目不同的是,我加入了 TypeScript ,其他的配置都是 vue-cli 本来配好的了。详情请看 vue-cli 官网 。6.1 安装及构建项目目录安装的依赖:安装过程选择的一些配置:搭建好之后,初始项目结构长这样:├── public // 静态页面├── src // 主目录 ├── assets // 静态资源 ├── components // 组件 ├── views // 页面 ├── App.vue // 页面主入口 ├── main.ts // 脚本主入口 ├── router.ts // 路由 ├── shims-tsx.d.ts // 相关 tsx 模块注入 ├── shims-vue.d.ts // Vue 模块注入 └── store.ts // vuex 配置├── tests // 测试用例├── .eslintrc.js // eslint 相关配置├── .gitignore // git 忽略文件配置├── babel.config.js // babel 配置├── postcss.config.js // postcss 配置├── package.json // 依赖└── tsconfig.json // ts 配置奔着 大型项目的结构 来改造项目结构,改造后 :├── public // 静态页面├── src // 主目录 ├── assets // 静态资源 ├── filters // 过滤 ├── store // vuex 配置 ├── less // 样式 ├── utils // 工具方法(axios封装,全局方法等) ├── views // 页面 ├── App.vue // 页面主入口 ├── main.ts // 脚本主入口 ├── router.ts // 路由 ├── shime-global.d.ts // 相关 全局或者插件 模块注入 ├── shims-tsx.d.ts // 相关 tsx 模块注入 ├── shims-vue.d.ts // Vue 模块注入, 使 TypeScript 支持 .vue 后缀的文件├── tests // 测试用例├── .eslintrc.js // eslint 相关配置├── postcss.config.js // postcss 配置├── .gitignore // git 忽略文件配置├── babel.config.js // preset 记录├── package.json // 依赖├── README.md // 项目 readme├── tsconfig.json // ts 配置└── vue.config.js // webpack 配置tsconfig.json 文件中指定了用来编译这个项目的根文件和编译选项。本项目的 tsconfig.json 配置如下 :{ // 编译选项 “compilerOptions”: { // 编译输出目标 ES 版本 “target”: “esnext”, // 采用的模块系统 “module”: “esnext”, // 以严格模式解析 “strict”: true, “jsx”: “preserve”, // 从 tslib 导入外部帮助库: 比如__extends,__rest等 “importHelpers”: true, // 如何处理模块 “moduleResolution”: “node”, // 启用装饰器 “experimentalDecorators”: true, “esModuleInterop”: true, // 允许从没有设置默认导出的模块中默认导入 “allowSyntheticDefaultImports”: true, // 定义一个变量就必须给它一个初始值 “strictPropertyInitialization” : false, // 允许编译javascript文件 “allowJs”: true, // 是否包含可以用于 debug 的 sourceMap “sourceMap”: true, // 忽略 this 的类型检查, Raise error on this expressions with an implied any type. “noImplicitThis”: false, // 解析非相对模块名的基准目录 “baseUrl”: “.”, // 给错误和消息设置样式,使用颜色和上下文。 “pretty”: true, // 设置引入的定义文件 “types”: [“webpack-env”, “mocha”, “chai”], // 指定特殊模块的路径 “paths”: { “@/”: [“src/”] }, // 编译过程中需要引入的库文件的列表 “lib”: [“esnext”, “dom”, “dom.iterable”, “scripthost”] }, // ts 管理的文件 “include”: [ “src/**/.ts”, “src//*.tsx”, “src//.vue”, “tests/**/.ts”, “tests/**/.tsx” ], // ts 排除的文件 “exclude”: [“node_modules”]}更多配置请看官网的 tsconfig.json 的 编译选项本项目的 vue.config.js:const path = require(“path”);const sourceMap = process.env.NODE_ENV === “development”;module.exports = { // 基本路径 publicPath: “./”, // 输出文件目录 outputDir: “dist”, // eslint-loader 是否在保存的时候检查 lintOnSave: false, // webpack配置 // see https://github.com/vuejs/vue-cli/blob/dev/docs/webpack.md chainWebpack: () => {}, configureWebpack: config => { if (process.env.NODE_ENV === “production”) { // 为生产环境修改配置… config.mode = “production”; } else { // 为开发环境修改配置… config.mode = “development”; } Object.assign(config, { // 开发生产共同配置 resolve: { extensions: [".js", “.vue”, “.json”, “.ts”, “.tsx”], alias: { vue$: “vue/dist/vue.js”, “@”: path.resolve(__dirname, “./src”) } } }); }, // 生产环境是否生成 sourceMap 文件 productionSourceMap: sourceMap, // css相关配置 css: { // 是否使用css分离插件 ExtractTextPlugin extract: true, // 开启 CSS source maps? sourceMap: false, // css预设器配置项 loaderOptions: {}, // 启用 CSS modules for all css / pre-processor files. modules: false }, // use thread-loader for babel & TS in production build // enabled by default if the machine has more than 1 cores parallel: require(“os”).cpus().length > 1, // PWA 插件相关配置 // see https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue/cli-plugin-pwa pwa: {}, // webpack-dev-server 相关配置 devServer: { open: process.platform === “darwin”, host: “localhost”, port: 3001, //8080, https: false, hotOnly: false, proxy: { // 设置代理 // proxy all requests starting with /api to jsonplaceholder “/api”: { // target: “https://emm.cmccbigdata.com:8443/", target: “http://localhost:3000/”, // target: “http://47.106.136.114/”, changeOrigin: true, ws: true, pathRewrite: { “^/api”: "” } } }, before: app => {} }, // 第三方插件配置 pluginOptions: { // … }};6.2 安装 element-ui本来想搭配 iview-ui 来用的,但后续还想把这个项目搞成 ssr 的,而 vue + typescript + iview + Nuxt.js 的服务端渲染还有不少坑, 而 vue + typescript + element + Nuxt.js 对 ssr 的支持已经不错了,所以选择了 element-ui 。安装:npm i element-ui -S按需引入, 借助 babel-plugin-component,我们可以只引入需要的组件,以达到减小项目体积的目的。npm install babel-plugin-component -D然后,将 babel.config.js 修改为:module.exports = { presets: ["@vue/app"], plugins: [ [ “component”, { libraryName: “element-ui”, styleLibraryName: “theme-chalk” } ] ]};接下来,如果你只希望引入部分组件,比如 Button 和 Select,那么需要在 main.js 中写入以下内容:import Vue from ‘vue’;import { Button, Select } from ’element-ui’;import App from ‘./App.vue’;Vue.component(Button.name, Button);Vue.component(Select.name, Select);/ 或写为 * Vue.use(Button) * Vue.use(Select) /new Vue({ el: ‘#app’, render: h => h(App)});6.3 完善项目目录与文件route使用路由懒加载功能。export default new Router({ mode: “history”, routes: [ { path: “/”, name: “home”, component: () => import(/ webpackChunkName: “home” / “./views/home.vue”) }, { path: “/articles”, name: “articles”, // route level code-splitting // this generates a separate chunk (articles.[hash].js) for this route // which is lazy-loaded when the route is visited. component: () => import(/ webpackChunkName: “articles” / “./views/articles.vue”) }, ]});utilsutils/utils.ts 常用函数的封装, 比如 事件的节流(throttle)与防抖(debounce)方法:// fn是我们需要包装的事件回调, delay是时间间隔的阈值export function throttle(fn: Function, delay: number) { // last为上一次触发回调的时间, timer是定时器 let last = 0, timer: any = null; // 将throttle处理结果当作函数返回 return function() { // 保留调用时的this上下文 let context = this; // 保留调用时传入的参数 let args = arguments; // 记录本次触发回调的时间 let now = +new Date(); // 判断上次触发的时间和本次触发的时间差是否小于时间间隔的阈值 if (now - last < delay) { // 如果时间间隔小于我们设定的时间间隔阈值,则为本次触发操作设立一个新的定时器 clearTimeout(timer); timer = setTimeout(function() { last = now; fn.apply(context, args); }, delay); } else { // 如果时间间隔超出了我们设定的时间间隔阈值,那就不等了,无论如何要反馈给用户一次响应 last = now; fn.apply(context, args); } };}utils/config.ts 配置文件,比如 github 授权登录的回调地址、client_id、client_secret 等。const config = { ‘oauth_uri’: ‘https://github.com/login/oauth/authorize', ‘redirect_uri’: ‘https://biaochenxuying.cn/login', ‘client_id’: ‘XXXXXXXXXX’, ‘client_secret’: ‘XXXXXXXXXX’,};// 本地开发环境下if (process.env.NODE_ENV === ‘development’) { config.redirect_uri = “http://localhost:3001/login” config.client_id = “502176cec65773057a9e” config.client_secret = “65d444de381a026301a2c7cffb6952b9a86ac235”}export default config;如果你的生产环境也要 github 登录授权的话,请在 github 上申请一个 Oauth App ,把你的 redirect_uri,client_id,client_secret 的信息填在 config 里面即可。具体详情请看我写的这篇文章 github 授权登录教程与如何设计第三方授权登录的用户表utils/urls.ts 请求接口地址,统一管理。// url的链接export const urls: object = { login: “login”, register: “register”, getArticleList: “getArticleList”,};export default urls;utils/https.ts axios 请求的封装。import axios from “axios”;// 创建axios实例let service: any = {};service = axios.create({ baseURL: “/api”, // api的base_url timeout: 50000 // 请求超时时间 });// request拦截器 axios的一些配置service.interceptors.request.use( (config: any) => { return config; }, (error: any) => { // Do something with request error console.error(“error:”, error); // for debug Promise.reject(error); });// respone拦截器 axios的一些配置service.interceptors.response.use( (response: any) => { return response; }, (error: any) => { console.error(“error:” + error); // for debug return Promise.reject(error); });export default service;把 urls 和 https 挂载到 main.ts 里面的 Vue 的 prototype 上面。import service from “./utils/https”;import urls from “./utils/urls”;Vue.prototype.$https = service; // 其他页面在使用 axios 的时候直接 this.$http 就可以了Vue.prototype.$urls = urls; // 其他页面在使用 urls 的时候直接 this.$urls 就可以了然后就可以统一管理接口,而且调用起来也很方便啦。比如下面 文章列表的请求。async handleSearch() { this.isLoading = true; const res: any = await this.$https.get(this.$urls.getArticleList, { params: this.params }); this.isLoading = false; if (res.status === 200) { if (res.data.code === 0) { const data: any = res.data.data; this.articlesList = […this.articlesList, …data.list]; this.total = data.count; this.params.pageNum++; if (this.total === this.articlesList.length) { this.isLoadEnd = true; } } else { this.$message({ message: res.data.message, type: “error” }); } } else { this.$message({ message: “网络错误!”, type: “error” }); } }store ( Vuex )一般大型的项目都有很多模块的,比如本项目中有公共信息(比如 token )、 用户模块、文章模块。├── modules // 模块 ├── user.ts // 用户模块 ├── article.ts // 文章模块 ├── types.ts // 类型└── index.ts // vuex 主入口store/index.ts 存放公共的信息,并导入其他模块import Vue from “vue”;import Vuex from “vuex”;import * as types from “./types”;import user from “./modules/user”;import article from “./modules/article”;Vue.use(Vuex);const initPageState = () => { return { token: "" };};const store = new Vuex.Store({ strict: process.env.NODE_ENV !== “production”, // 具体模块 modules: { user, article }, state: initPageState(), mutations: { [types.SAVE_TOKEN](state: any, pageState: any) { for (const prop in pageState) { state[prop] = pageState[prop]; } } }, actions: {}});export default store;types.ts// 公共 tokenexport const SAVE_TOKEN = “SAVE_TOKEN”;// 用户export const SAVE_USER = “SAVE_USER”;user.tsimport * as types from “../types”;const initPageState = () => { return { userInfo: { _id: “”, name: “”, avator: "" } };};const user = { state: initPageState(), mutations: { [types.SAVE_USER](state: any, pageState: any) { for (const prop in pageState) { state[prop] = pageState[prop]; } } }, actions: {}};export default user;7. markdown 渲染markdown 渲染效果图: markdown 渲染 采用了开源的 marked, 代码高亮用了 highlight.js 。用法:第一步:npm i marked highlight.js –savenpm i marked highlight.js –save第二步: 导入封装成 markdown.js,将文章详情由字符串转成 html, 并抽离出文章目录。marked 的封装 得感谢这位老哥。const highlight = require(“highlight.js”);const marked = require(“marked”);const tocObj = { add: function(text, level) { var anchor = #toc${level}${++this.index}; this.toc.push({ anchor: anchor, level: level, text: text }); return anchor; }, // 使用堆栈的方式处理嵌套的ul,li,level即ul的嵌套层次,1是最外层 // <ul> // <li></li> // <ul> // <li></li> // </ul> // <li></li> // </ul> toHTML: function() { let levelStack = []; let result = “”; const addStartUL = () => { result += ‘<ul class=“anchor-ul” id=“anchor-fix”>’; }; const addEndUL = () => { result += “</ul>\n”; }; const addLI = (anchor, text) => { result += ‘<li><a class=“toc-link” href="#’ + anchor + ‘">’ + text + “<a></li>\n”; }; this.toc.forEach(function(item) { let levelIndex = levelStack.indexOf(item.level); // 没有找到相应level的ul标签,则将li放入新增的ul中 if (levelIndex === -1) { levelStack.unshift(item.level); addStartUL(); addLI(item.anchor, item.text); } // 找到了相应level的ul标签,并且在栈顶的位置则直接将li放在此ul下 else if (levelIndex === 0) { addLI(item.anchor, item.text); } // 找到了相应level的ul标签,但是不在栈顶位置,需要将之前的所有level出栈并且打上闭合标签,最后新增li else { while (levelIndex–) { levelStack.shift(); addEndUL(); } addLI(item.anchor, item.text); } }); // 如果栈中还有level,全部出栈打上闭合标签 while (levelStack.length) { levelStack.shift(); addEndUL(); } // 清理先前数据供下次使用 this.toc = []; this.index = 0; return result; }, toc: [], index: 0};class MarkUtils { constructor() { this.rendererMD = new marked.Renderer(); this.rendererMD.heading = function(text, level, raw) { var anchor = tocObj.add(text, level); return &lt;h${level} id=${anchor}&gt;${text}&lt;/h${level}&gt;\n; }; highlight.configure({ useBR: true }); marked.setOptions({ renderer: this.rendererMD, headerIds: false, gfm: true, tables: true, breaks: false, pedantic: false, sanitize: false, smartLists: true, smartypants: false, highlight: function(code) { return highlight.highlightAuto(code).value; } }); } async marked(data) { if (data) { let content = await marked(data); // 文章内容 let toc = tocObj.toHTML(); // 文章目录 return { content: content, toc: toc }; } else { return null; } }}const markdown = new MarkUtils();export default markdown;第三步: 使用import markdown from “@/utils/markdown”;// 获取文章详情async handleSearch() { const res: any = await this.$https.post( this.$urls.getArticleDetail, this.params ); if (res.status === 200) { if (res.data.code === 0) { this.articleDetail = res.data.data; // 使用 marked 转换 const article = markdown.marked(res.data.data.content); article.then((response: any) => { this.articleDetail.content = response.content; this.articleDetail.toc = response.toc; }); } else { // … } else { // … } }// 渲染<div id=“content” class=“article-detail” v-html=“articleDetail.content”></div>第四步:引入 monokai_sublime 的 css 样式<link href=“http://cdn.bootcss.com/highlight.js/8.0/styles/monokai_sublime.min.css" rel=“stylesheet”>第五步:对 markdown 样式的补充如果不补充样式,是没有黑色背景的,字体大小等也会比较小,图片也不会居中显示/对 markdown 样式的补充/pre { display: block; padding: 10px; margin: 0 0 10px; font-size: 14px; line-height: 1.42857143; color: #abb2bf; background: #282c34; word-break: break-all; word-wrap: break-word; overflow: auto;}h1,h2,h3,h4,h5,h6{ margin-top: 1em; / margin-bottom: 1em; /}strong { font-weight: bold;}p > code:not([class]) { padding: 2px 4px; font-size: 90%; color: #c7254e; background-color: #f9f2f4; border-radius: 4px;}p img{ / 图片居中 */ margin: 0 auto; display: flex;}#content { font-family: “Microsoft YaHei”, ‘sans-serif’; font-size: 16px; line-height: 30px;}#content .desc ul,#content .desc ol { color: #333333; margin: 1.5em 0 0 25px;}#content .desc h1, #content .desc h2 { border-bottom: 1px solid #eee; padding-bottom: 10px;}#content .desc a { color: #009a61;}8. 注意点关于 页面对于 关于 的页面,其实是一篇文章来的,根据文章类型 type 来决定的,数据库里面 type 为 3 的文章,只能有一篇就是 博主介绍 ;达到了想什么时候修改内容都可以。所以当 当前路由 === ‘/about’ 时就是请求类型为 博主介绍 的文章。type: 3, // 文章类型: 1:普通文章;2:是博主简历;3 :是博主简介;移动端适配移动端使用 rem 单位适配。// 屏幕适配( window.screen.width / 移动端设计稿宽 * 100)也即是 (window.screen.width / 750 * 100) ——*100 为了方便计算。即 font-size 值是手机 deviceWidth 与设计稿比值的 100 倍document.getElementsByTagName(‘html’)[0].style.fontSize = window.screen.width / 7.5 + ‘px’;如上:通过查询屏幕宽度,动态的设置 html 的 font-size 值,移动端的设计稿大多以宽为 750 px 来设置的。比如在设计图上一个 150 * 250 的盒子(单位 px):原本在 css 中的写法:width: 150px;heigth: 250px;通过上述换算后,在 css 中对应的 rem 值只需要写:width: 1.5rem; // 150 / 100 remheigth: 2.5rem; // 250 / 100 rem如果你的移动端的设计稿是以宽为 1080 px 来设置的话,就用 window.screen.width / 10.8 吧。9. 踩坑记1. 让 vue 识别全局方法/变量我们经常在 main.ts 中给 vue.prototype 挂载实例或者内容,以方便在组件里面使用。import service from “./utils/https”;import urls from “./utils/urls”;Vue.prototype.$https = service; // 其他页面在使用 axios 的时候直接 this.$http 就可以了Vue.prototype.$urls = urls; // 其他页面在使用 urls 的时候直接 this.$urls 就可以了然而当你在组件中直接 this.$http 或者 this.$urls 时会报错的,那是因为 $http 和 $urls 属性,并没有在 vue 实例中声明。再比如使用 Element-uI 的 meesage。import { Message } from “element-ui”;Vue.prototype.$message = Message;之前用法如下图: this.$message({ message: ‘恭喜你,这是一条成功消息’, type: ‘success’ })然而还是会报错的。再比如 监听路由的变化:import { Vue, Watch } from “vue-property-decorator”;import Component from “vue-class-component”;import { Route } from “vue-router”;@Componentexport default class App extends Vue { @Watch("$route”) routeChange(val: Route, oldVal: Route) { // do something }}只是这样写的话,监听 $route 还是会报错的。想要以上三种做法都正常执行,就还要补充如下内容:在 src 下的 shims-vue.d.ts 中加入要挂载的内容。 表示 vue 里面的 this 下有这些东西。import VueRouter, { Route } from “vue-router”;declare module “vue/types/vue” { interface Vue { $router: VueRouter; // 这表示this下有这个东西 $route: Route; $https: any; // 不知道类型就定为 any 吧(偷懒) $urls: any; $Message: any; }}2. 引入的模块要声明比如 在组件里面使用 window.document 或者 document.querySelector 的时候会报错的,npm run build 不给通过。再比如:按需引用 element 的组件与动画组件:import { Button } from “element-ui”;import CollapseTransition from “element-ui/lib/transitions/collapse-transition”;npm run serve 时可以执行,但是在 npm run build 的时候,会直接报错的,因为没有声明。正确做法:我在 src 下新建一个文件 shime-global.d.ts ,加入内容如下:// 声明全局的 window ,不然使用 window.XX 时会报错declare var window: Window;declare var document: Document;declare module “element-ui/lib/transitions/collapse-transition”;declare module “element-ui”;当然,这个文件你加在其他地方也可以,起其他名字都 OK。但是即使配置了以上方法之后,有些地方使用 document.XXX ,比如 document.title 的时候,npm run build 还是通过不了,所以只能这样了:<script lang=“ts”>// 在用到 document.XXX 的文件中声明一下即可declare var document: any;// 此处省略 XXXX 多的代码</script>3. this 的类型检查比如之前的 事件的节流(throttle)与防抖(debounce)方法:export function throttle(fn: Function, delay: number) { return function() { // 保留调用时的 this 上下文 let context = this;}function 里面的 this 在 npm run serve 时会报错的,因为 tyescript 检测到它不是在类(class)里面。正确做法:在根目录的 tsconfig.json 里面加上 “noImplicitThis”: false ,忽略 this 的类型检查。// 忽略 this 的类型检查, Raise error on this expressions with an implied any type.“noImplicitThis”: false,4. import 的 .vue 文件import .vue 的文件的时候,要补全 .vue 的后缀,不然 npm run build 会报错的。比如:import Nav from “@/components/nav”; // @ is an alias to /srcimport Footer from “@/components/footer”; // @ is an alias to /src要修改为:import Nav from “@/components/nav.vue”; // @ is an alias to /srcimport Footer from “@/components/footer.vue”; // @ is an alias to /src5. 装饰器 @Component报错。<script lang=“ts”>import { Vue, Component } from “vue-property-decorator”;export default class LoadingCustom extends Vue {}</script>以下才是正确,因为这里的 Vue 是从 vue-property-decorator import 来的。<script lang=“ts”>import { Vue, Component } from “vue-property-decorator”;@Componentexport default class LoadingCustom extends Vue {}</script>6. 路由的组件导航守卫失效vue-class-component 官网里面的路由的导航钩子的用法是没有效果的 Adding Custom Hooks路由的导航钩子不属于 Vue 本身,这会导致 class 组件转义到配置对象时导航钩子无效,因此如果要使用导航钩子需要在 router 的配置里声明(网上别人说的,还没实践,不确定是否可行)。7. tsconfig.json 的 strictPropertyInitialization 设为 false,不然你定义一个变量就必须给它一个初始值。position: sticky;本项目中的文章详情的目录就是用了 sticky。.anchor { position: sticky; top: 213px; margin-top: 213px;}position:sticky 是 css 定位新增属性;可以说是相对定位 relative 和固定定位 fixed 的结合;它主要用在对 scroll 事件的监听上;简单来说,在滑动过程中,某个元素距离其父元素的距离达到 sticky 粘性定位的要求时(比如 top:100px );position:sticky 这时的效果相当于 fixed 定位,固定到适当位置。用法像上面那样用即可,但是有使用条件:1、父元素不能 overflow:hidden 或者 overflow:auto 属性。2、必须指定 top、bottom、left、right 4 个值之一,否则只会处于相对定位3、父元素的高度不能低于 sticky 元素的高度4、sticky 元素仅在其父元素内生效8. eslint 报找不到文件和装饰器的错App.vue 中只是写了引用文件而已,而且 webpack 和 tsconfig.josn 里面已经配置了别名了的。import Nav from “@/components/nav.vue”; // @ is an alias to /srcimport Slider from “@/components/slider.vue”; // @ is an alias to /srcimport Footer from “@/components/footer.vue”; // @ is an alias to /srcimport ArrowUp from “@/components/arrowUp.vue”; // @ is an alias to /srcimport { isMobileOrPc } from “@/utils/utils”;但是,还是会报如下的错:只是代码不影响文件的打包,而且本地与生产环境的代码也正常,没报错而已。这个 eslint 的检测目前还没找到相关的配置可以把这些错误去掉。9. 路由模式修改为 history因为文章详情页面有目录,点击目录时定位定相应的内容,但是这个目录定位内容是根据锚点来做的,如果路由模式为 hash 模式的话,本来文章详情页面的路由就是 #articleDetail 了,再点击目录的话(比如 #title2 ),会在 #articleDetail 后面再加上 #title2,一刷新会找不到这个页面的。10. Build Setup # clonegit clone https://github.com/biaochenxuying/blog-vue-typescript.git# cdcd blog-vue-typescript# install dependenciesnpm install# Compiles and hot-reloads for developmentnpm run serve# Compiles and minifies for productionnpm run build### Run your testsnpm run test### Lints and fixes filesnpm run lint### Run your unit testsnpm run test:unitCustomize configurationSee Configuration Reference.如果要看有后台数据完整的效果,是要和后台项目 blog-node 一起运行才行的,不然接口请求会失败。虽然引入了 mock 了,但是还没有时间做模拟数据,想看具体效果,请稳步到我的网站上查看 https://biaochenxuying.cn11. 项目地址与系列相关文章基于 Vue + TypeScript + Element 的 blog-vue-typescript 前台展示: https://github.com/biaochenxuying/blog-vue-typescript基于 react + node + express + ant + mongodb 的博客前台,这个是笔者之前做的,效果和这个类似,地址如下: blog-react 前台展示: https://github.com/biaochenxuying/blog-react推荐阅读 :本博客系统的系列文章:react + node + express + ant + mongodb 的简洁兼时尚的博客网站react + Ant Design + 支持 markdown 的 blog-react 项目文档说明基于 node + express + mongodb 的 blog-node 项目文档说明服务器小白的我,是如何将node+mongodb项目部署在服务器上并进行性能优化的github 授权登录教程与如何设计第三方授权登录的用户表一次网站的性能优化之路 – 天下武功,唯快不破Vue + TypeScript + Element 搭建简洁时尚的博客网站及踩坑记12. 最后笔者也是初学 TS ,如果文章有错的地方,请指出,感谢。一开始用 Vue + TS 来搭建时,我也是挺抵触的,因为踩了好多坑,而且很多类型检查方面也挺烦人。后面解决了,明白原理之后,是越用越爽,哈哈。权衡如何更好的利用 JS 的动态性和 TS 的静态特质,我们需要结合项目的实际情况来进行综合判断。一些建议:如果是中小型项目,且生命周期不是很长,那就直接用 JS 吧,不要被 TS 束缚住了手脚。如果是大型应用,且生命周期比较长,那建议试试 TS。如果是框架、库之类的公共模块,那更建议用 TS 了。至于到底用不用TS,还是要看实际项目规模、项目生命周期、团队规模、团队成员情况等实际情况综合考虑。其实本项目也是小项目来的,其实并不太适合加入 TypeScript ,不过这个项目是个人的项目,是为了练手用的,所以就无伤大大雅。未来,class-compoent 也将成为主流,现在写 TypeScript 以后进行 3.0 的迁移会更加方便。每天下班后,用几个晚上的时间来写这篇文章,码字不易,如果您觉得这篇文章不错或者对你有所帮助,请给个赞或者星吧,你的点赞就是我继续创作的最大动力。参考文章:vue + typescript 项目起手式TypeScript + 大型项目实战欢迎关注以下公众号,学到不一样的 武功秘籍 !关注公众号并回复 福利 可领取免费学习资料,福利详情请猛戳: 免费资源获取–Python、Java、Linux、Go、node、vue、react、javaScript ...

March 31, 2019 · 14 min · jiezi

# react-router v4 刷新出现找不到页面(NO FOUND)解决方案

react-router v4 刷新页面出现找不到问题解决方案原因### 浏览器被刷新相当于重新请求了服务端的 page 接口,当后端没有这个接口时,就没有document文档返回,这时url 并没有被js 观察处理解决如果是使用webpack-dev-server,请将 historyApiFallback: true 这个配置加入至 devServer 中. 以及在output 中配置 publicPath: ‘/‘如果是使用自定义的node服务器的话,需自己手写一个404接口. 将所有的url 都返回到index.html文档实例koaconst koaWebpack = require(‘koa-webpack’);async startService() {const middleware = await koaWebpack({ config: this.webpackConfig });this.app.use(middleware);app.use(async ctx => { const filename = path.resolve(this.webpackConfig.output.path, ‘index.html’); ctx.response.type = ‘html’; ctx.response.body = middleware.devMiddleware.fileSystem.createReadStream( filename );});this.app.listen(this.port, () => { console.log(当前服务器已启动, http://${this.host}:${this.port});});}[参考地址][1]

March 31, 2019 · 1 min · jiezi

前端面试题 -- webpack

前言上一篇 前端面试题-小程序随着前端的不断发展,现代前端开发的复杂度和规模越来越庞大。工程化的思想催生了很多流行框架的进程,作为现在最流行的前端构建工具–webpack,很多面试、工作场景中都会出现了它的身影。所以对于现在的前端来说,了解并能够使用webpack,无论对个人技能或者职场求职来说,都是一种有力的提升感兴趣的小伙伴可以点击 这里,查看完整版前端面试题如果文章中有出现纰漏、错误之处,还请看到的小伙伴留言指正,先行谢过以下 ↓1. 对webpack的了解官方文档本质上,webpack 是一个现代 JavaScript 应用程序的静态模块打包器(module bundler),将项目当作一个整体,通过一个给定的的主文件,webpack将从这个文件开始找到你的项目的所有依赖文件,使用loaders处理它们,最后打包成一个或多个浏览器可识别的js文件核心概念:入口(entry)入口起点(entry point)指示 webpack 应该使用哪个模块,来作为构建其内部依赖图的开始可以通过在 webpack 配置中配置 entry 属性,来指定一个入口起点(或多个入口起点)module.exports = { entry: ‘./path/to/my/entry/file.js’};输出(output)output 属性告诉 webpack 在哪里输出它所创建的 bundles ,以及如何命名这些文件,默认值为 ./distloaderloader 让 webpack 能够去处理那些非 JavaScript 文件(webpack 自身只理解 JavaScript)插件(plugins)loader 被用于转换某些类型的模块,而插件则可以用于执行范围更广的任务。插件的范围包括,从打包优化和压缩,一直到重新定义环境中的变量模式通过选择 development 或 production 之中的一个,来设置 mode 参数,你可以启用相应模式下的 webpack 内置的优化module.exports = { mode: ‘production’};2. webpack,里面的webpack.config.js怎么配置let webpack = require(‘webpack’);module.exports = { entry:’./entry.js’, //入口文件 output:{ //node.js中__dirname变量获取当前模块文件所在目录的完整绝对路径 path:__dirname, //输出位置 filename:‘build.js’ //输入文件 }, module:{ // 关于模块的加载相关,我们就定义在module.loaders中 // 这里通过正则表达式去匹配不同后缀的文件名,然后给它们定义不同的加载器。 // 比如说给less文件定义串联的三个加载器(!用来定义级联关系): rules:[ { test:/.css$/, //支持正则 loader:‘style-loader!css-loader’ } ] }, //配置服务 devServer:{ hot:true, //启用热模块替换 inline:true //此模式支持热模块替换:热模块替换的好处是只替换更新的部分,而不是页面重载. }, //其他解决方案配置 resolve:{ extensions:[’’,’.js’,’.json’,’.css’,’.scss’] }, //插件 plugins:[ new webpack.BannerPlugin(‘This file is create by baibai’) ]}3. webpack本地开发怎么解决跨域的下载 webpack-dev-server 插件配置 webpack.config.js 文件// webpack.config.jsvar WebpackDevServer = require(“webpack-dev-server”);module.exports = { … devServer: { … port: ‘8088’, //设置端口号 // 代理设置 proxy: { ‘/api’: { target: ‘http://localhost:80/index.php’, // 目标代理 pathRewrite: {’^/api’ : ‘’}, // 重写路径 secure: false, // 是否接受运行在 HTTPS 上 } } }}4. 如何配置多入口文件配置多个入口文件entry: { home: resolve(__dirname, “src/home/index.js”), about: resolve(__dirname, “src/about/index.js”)}5. webpack与grunt、gulp的不同三者都是前端构建工具grunt 和 gulp 是基于任务和流的。找到一个(或一类)文件,对其做一系列链式操作,更新流上的数据, 整条链式操作构成了一个任务,多个任务就构成了整个web的构建流程webpack 是基于入口的。webpack 会自动地递归解析入口所需要加载的所有资源文件,然后用不同的 Loader 来处理不同的文件,用 Plugin 来扩展 webpack 功能webpack 与前者最大的不同就是支持代码分割,模块化(AMD,CommonJ,ES2015),全局分析为什么选择webpack6. 有哪些常见的Loader?他们是解决什么问题的css-loader:加载 CSS,支持模块化、压缩、文件导入等特性style-loader:把 CSS 代码注入到 JavaScript 中,通过 DOM 操作去加载 CSSslint-loader:通过 SLint 检查 JavaScript 代码babel-loader:把 ES6 转换成 ES5file-loader:把文件输出到一个文件夹中,在代码中通过相对 URL 去引用输出的文件url-loader:和 file-loader 类似,但是能在文件很小的情况下以 base64 的方式把文件内容注入到代码中去7. 有哪些常见的Plugin?他们是解决什么问题的define-plugin:定义环境变量commons-chunk-plugin:提取公共代码8. Loader和Plugin的不同loader 加载器Webpack 将一切文件视为模块,但是 webpack 原生是只能解析 js 文件. Loader 的作用是让 webpack 拥有了加载和解析非 JavaScript 文件的能力在 module.rules 中配置,也就是说他作为模块的解析规则而存在,类型为数组Plugin 插件扩展 webpack 的功能,让 webpack 具有更多的灵活性在 plugins 中单独配置。类型为数组,每一项是一个 plugin 的实例,参数都通过构造函数传入9. webpack的构建流程是什么初始化参数:从配置文件和 Shell 语句中读取与合并参数,得出最终的参数开始编译:用上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,执行对象的 run 方法开始执行编译确定入口:根据配置中的 entry 找出所有的入口文件编译模块:从入口文件出发,调用所有配置的 Loader 对模块进行翻译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理完成模块编译:在经过第4步使用 Loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的依赖关系输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统在以上过程中,Webpack 会在特定的时间点广播出特定的事件,插件在监听到感兴趣的事件后会执行特定的逻辑,并且插件可以调用 Webpack 提供的 API 改变 Webpack 的运行结果10. 是否写过Loader和Plugin?描述一下编写loader或plugin的思路编写 Loader 时要遵循单一原则,每个 Loader 只做一种"转义"工作。 每个 Loader 的拿到的是源文件内容(source),可以通过返回值的方式将处理后的内容输出,也可以调用 this.callback() 方法,将内容返回给 webpack 。 还可以通过 this.async() 生成一个 callback 函数,再用这个 callback` 将处理后的内容输出出去相对于 Loader 而言,Plugin 的编写就灵活了许多。 webpack 在运行的生命周期中会广播出许多事件,Plugin 可以监听这些事件,在合适的时机通过 Webpack 提供的 API 改变输出结果11. webpack的热更新是如何做到的?说明其原理具体可以参考 这里12. 如何利用webpack来优化前端性能压缩代码。删除多余的代码、注释、简化代码的写法等等方式利用 CDN 加速。在构建过程中,将引用的静态资源路径修改为 CDN 上对应的路径删除死代码 Tree Shaking)。将代码中永远不会走到的片段删除掉优化图片,对于小图可以使用 base64 的方式写入文件中按照路由拆分代码,实现按需加载,提取公共代码给打包出来的文件名添加哈希,实现浏览器缓存文件13. 如何提高webpack的构建速度参考 这里14. 怎么配置单页应用?怎么配置多页应用单页应用可以理解为 webpack 的标准模式,直接在 entry 中指定单页应用的入口即可多页应用的话,可以使用 webpack 的 AutoWebPlugin 来完成简单自动化的构建,但是前提是项目的目录结构必须遵守他预设的规范15. 什么是bundle,什么是chunk,什么是modulebundle 是由 webpack 打包出来的文件,chunk 是指 webpack 在进行模块的依赖分析的时候,代码分割出来的代码块。module是开发中的单个模块后记前端发展日新月异,只有保持不断学习的姿态,才能走的更远希望这些面试题可以帮助大家温故知新、查缺补漏,不断充实自己的专业技能,走的更远最后推荐一波 GitHub前端面试题完整版,欢迎小伙伴们 star 关注 不定期更新,期待同行以上 ...

March 29, 2019 · 2 min · jiezi

webpack4心得编译篇&体积篇

背景众所周知,webpack作为主流的前端项目利器,从编译到打包提供了很多方便的功能。本文主要从编译和体积两个篇章阐述笔者总结的实践心得,希望对大家有帮助。编译篇vendor文件单独打包vendor文件即依赖库文件,一般在项目中很少改动。单独打包可以在后续的项目迭代过程中,保证vendor文件可从客户端缓存读取,提升客户端的访问体验。 解决方案:通过在vendor.config.js文件中定义,在webpack.config.{evn}.js中引用来使用。 vendor.config.js示例module.exports = { entry: { vendor: [‘react’, ‘react-dom’, ‘react-redux’, ‘react-router-dom’, ‘react-loadable’, ‘axios’], }};vendor文件预打包vendor单独打包之后,还是有一个问题。编译的过程中,每次都需要对vendor文件进行打包,其实这一块要是可以提前打包好,那后续编译的时候,就可以节约这部分的时间了。 解决方案:定义webpack.dll.config.js,使用 DLLPlugin 提前执行打包,然后在webpack.config.{evn}.js通过 DLLReferencePlugin 引入打包好的文件,最后使用AddAssetHtmlPlugin往html里注入vendor文件路径webpack.dll.config.js示例const TerserPlugin = require(’terser-webpack-plugin’);const CleanWebpackPlugin = require(“clean-webpack-plugin”);const webpack = require(‘webpack’);const path = require(‘path’);const dllDist = path.join(__dirname, ‘dist’);module.exports = { entry: { vendor: [‘react’, ‘react-dom’, ‘react-redux’, ‘react-router-dom’, ‘react-loadable’, ‘axios’, ‘moment’], }, output: { path: const dllDist = path.join(__dirname, ‘dist’), filename: ‘[name]-[hash].js’, library: ‘[name]’, }, optimization: { minimizer: [ new TerserPlugin({ terserOptions: { parse: { ecma: 8, }, compress: { ecma: 5, warnings: false, comparisons: false, inline: 2, }, mangle: { safari10: true, }, output: { ecma: 5, comments: false, ascii_only: true, }, }, parallel: true, cache: true, sourceMap: false, }), ], }, plugins: [ new CleanWebpackPlugin([".js"], { // 清除之前的dll文件 root: dllDist, }), new webpack.IgnorePlugin(/^./locale$/, /moment$/), new webpack.DllPlugin({ path: path.join(__dirname, ‘dll’, ‘[name]-manifest.json’), name: ‘[name]’, }), ]};webpack.config.prod.js片段const manifest = require(’./dll/vendor-manifest.json’);const AddAssetHtmlPlugin = require(‘add-asset-html-webpack-plugin’);…plugins: [ // webpack读取到vendor的manifest文件对于vendor的依赖不会进行编译打包 new webpack.DllReferencePlugin({ manifest, }), // 往html中注入vendor js new AddAssetHtmlPlugin([{ publicPath: “/view/static/js”, // 注入到html中的路径 outputPath: “../build/static/js”, // 最终输出的目录 filepath: path.resolve(__dirname, ‘./dist/.js’), includeSourcemap: false, typeOfAsset: “js” }]),]js并行编译与压缩webpack对文件的编译处理是单进程的,但实际上我们的编译机器通常是多核多进程,如果可以充分利用cpu的运算力,可以提升很大的编译速度。 解决方案:使用happypack进行多进程构建,使用webpack4内置的TerserPlugin并行模式进行js的压缩。 说明:happypack原理可参考http://taobaofed.org/blog/201…webpack.config.prod.js片段const HappyPack = require(‘happypack’);// 采用多进程,进程数由CPU核数决定const happThreadPool = HappyPack.ThreadPool({ size: os.cpus().length });…optimization: { minimizer: [ new TerserPlugin({ terserOptions: { parse: { ecma: 8, }, compress: { ecma: 5, warnings: false, comparisons: false, inline: 2, }, mangle: { safari10: true, }, output: { ecma: 5, comments: false, ascii_only: true, }, }, parallel: true, cache: true, sourceMap: false, }), ]},module: { rules: [ { test: /.css$/, oneOf: [ { test: /.(js|mjs|jsx)$/, include: paths.appSrc, loader: ‘happypack/loader’, options: { cacheDirectory: true, }, }, ] } ]},plugins: [ new HappyPack({ threadPool: happThreadPool, loaders: [{ loader: ‘babel-loader’, }] }),]体积篇按需加载当js页面特别多的时候,如果都打包成一个文件,那么很影响访问页面访问的速度。理想的情况下,是到相应页面的时候才下载相应页面的js。解决方案:使用import(‘path/to/module’) -> Promise。调用 import() 之处,被作为分离的模块起点,意思是,被请求的模块和它引用的所有子模块,会分离到一个单独的 chunk 中。说明: 老版本使用require.ensure(dependencies, callback)进行按需加载,webpack > 2.4 的版本此方法已经被import()取代一般例子按需加载demo,在非本地的环境下开启监控上报if (process.env.APP_ENV !== ’local’) { import("./utils/emonitor").then(({emonitorReport}) => { emonitorReport(); });}react例子react页面按需加载,可参考http://react.html.cn/docs/cod…,里面提到的React.lazy,React.Suspense是在react 16.6版本之后才有的新特性,对于老版本,官方依然推荐使用react-loadable实现路由懒加载react-loadable示例import React, { Component } from ‘react’;import { Route, Switch } from ‘react-router-dom’;import Loadable from ‘react-loadable’;import React, { Component } from ‘react’;// 经过包装的组件会在访问相应的页面时才异步地加载相应的jsconst Home = Loadable({ loader: () => import(’./page/Home’), loading: (() => null), delay: 1000,});import NotFound from ‘@/components/pages/NotFound’;class CRouter extends Component { render() { return ( <Switch> <Route exact path=’/’ component={Home}/> {/* 如果没有匹配到任何一个Route, <NotFound>会被渲染*/} <Route component={NotFound}/> </Switch> ) }}export default CRoutervue例子vue页面按需加载,可参考https://router.vuejs.org/zh/g…示例// 下面2行代码,没有指定webpackChunkName,每个组件打包成一个js文件。const ImportFuncDemo1 = () => import(’../components/ImportFuncDemo1’)const ImportFuncDemo2 = () => import(’../components/ImportFuncDemo2’)// 下面2行代码,指定了相同的webpackChunkName,会合并打包成一个js文件。// const ImportFuncDemo = () => import(/* webpackChunkName: ‘ImportFuncDemo’ / ‘../components/ImportFuncDemo’)// const ImportFuncDemo2 = () => import(/ webpackChunkName: ‘ImportFuncDemo’ */ ‘../components/ImportFuncDemo2’)export default new Router({ routes: [ { path: ‘/importfuncdemo1’, name: ‘ImportFuncDemo1’, component: ImportFuncDemo1 }, { path: ‘/importfuncdemo2’, name: ‘ImportFuncDemo2’, component: ImportFuncDemo2 } ]})css预加载做完按需加载之后,假如定义的分离点里包含了css文件,那么相关css样式也会被打包进js chunk里,并通过URL.createObjectURL(blob)的方式加载到页面中。假如n个页面引用了共同的css样式,无形中也增加n倍的 css in js体积。通过css预加载,把共同css提炼到html link标签里,可以优化这部分的体积。解决方案:把分离点里的页面css引用(包括less和sass)提炼到index.less中,在index.js文件中引用。假如使用到库的less文件特别多,可以定义一个cssVendor.js,在index.js中引用,并在webpack config中添加一个entry以配合MiniCssExtractPlugin做css抽离。P.S. 假如用到antd或其他第三方UI库,按需加载的时候记得把css引入选项取消,把 style: true选项删掉示例cssVendor片段// 全局引用的组件的样式预加载,按需引用,可优化异步加载的chunk js体积// Rowimport ‘antd/es/row/style/index.js’;// Colimport ‘antd/es/col/style/index.js’;// Cardimport ‘antd/es/card/style/index.js’;// Iconimport ‘antd/es/icon/style/index.js’;// Modalimport ‘antd/es/modal/style/index.js’;// messageimport ‘antd/es/message/style/index.js’;…webpack.config.production片段 entry: { main: [paths.appIndexJs, paths.cssVendorJs] }, plugins: [ new HappyPack({ threadPool: happThreadPool, loaders: [{ loader: ‘babel-loader’, options: { customize: require.resolve( ‘babel-preset-react-app/webpack-overrides’ ), plugins: [ [ require.resolve(‘babel-plugin-named-asset-import’), { loaderMap: { svg: { ReactComponent: ‘@svgr/webpack?-prettier,-svgo![path]’, }, }, }, ], [‘import’, { libraryName: ‘antd’, libraryDirectory: ’es’ }, ], ], cacheDirectory: true, cacheCompression: true, compact: true, }, }], })]按需打包我们在项目的开发中经常会引用一些第三方库,例如antd,lodash。这些库在我们的项目中默认是全量引入的,但其实我们只用到库里的某些组件或者是某些函数,那么按需只打包我们引用的组件或函数就可以减少js相当大一部分的体积。解决方案:使用babel-plugin-import插件来实现按需打包,具体使用方式可参考https://github.com/ant-design…示例{ test: /.(js|jsx)$/, include: paths.appSrc, loader: require.resolve(‘babel-loader’), exclude: /node_modules/, options: { plugins: [ [‘import’, [ { libraryName: ’lodash’, libraryDirectory: ‘’, “camel2DashComponentName”: false, }, { libraryName: ‘antd’, style: true } ] ], ], compact: true, },}忽略不必要的文件有些包含多语言的库会将所有本地化内容和核心功能一起打包,于是打包出来的js里会包含很多多语言的配置文件,这些配置文件如果不打包进来,也可以减少js的体积。解决方案:使用IgnorePlugin插件忽略指定资源路径的打包示例new webpack.IgnorePlugin(/^./locale$/, /moment$/)压缩压缩是一道常规的生产工序,前端项目编译出来的文件经过压缩混淆,可以把体积进一步缩小。解决方案:使用TerserPlugin插件进行js压缩,使用OptimizeCSSAssetsPlugin插件css压缩说明:webpack4之前js压缩推荐使用ParalleUglifyPlugin插件,它在UglifyJsPlugin的基础上做了多进程并行处理的优化,速度更快;css压缩推荐使用cssnano,它基于PostCSS。因为css-loader已经将其内置了,要开启cssnano去压缩代码只需要开启css-loader的minimize选项。示例minimizer: [ new TerserPlugin({ terserOptions: { parse: { ecma: 8, }, compress: { ecma: 5, warnings: false, comparisons: false, inline: 2, }, mangle: { safari10: true, }, output: { ecma: 5, comments: false, ascii_only: true, }, }, parallel: true, cache: true, sourceMap: shouldUseSourceMap, }), new OptimizeCSSAssetsPlugin({ cssProcessorOptions: { parser: safePostCssParser, map: shouldUseSourceMap ? { inline: false, annotation: true, } : false, }, }),]抽离共同文件在很多chunks里,有相同的依赖,把这些依赖抽离为一个公共的文件,则可以有效地减少资源的体积,并可以充分利用浏览器缓存。解决方案:使用SplitChunksPlugin抽离共同文件P.S. webpack4使用SplitChunksPlugin代替了CommonsChunkPlugin 示例optimization: { splitChunks: { chunks: ‘all’, name: false }}SplitChunksPlugin的具体配置可参考 https://juejin.im/post/5af15e…开启Scope Hoisting(作用域提升)Scope Hoisting 是webpack3中推出的新功能,可以把依赖的代码直接注入到入口文件里,减少了函数作用域的声明,也减少了js体积和内存开销举个栗子假如现在有两个文件分别是 util.js:export default ‘Hello,Webpack’;和入口文件 main.js:import str from ‘./util.js’;console.log(str);以上源码用 Webpack 打包后输出中的部分代码如下:[ (function (module, webpack_exports, webpack_require) { var WEBPACK_IMPORTED_MODULE_0__util_js = webpack_require(1); console.log(WEBPACK_IMPORTED_MODULE_0__util_js[“a”]); }), (function (module, webpack_exports, webpack_require) { webpack_exports[“a”] = (‘Hello,Webpack’); })]在开启 Scope Hoisting 后,同样的源码输出的部分代码如下:[ (function (module, webpack_exports, webpack_require) { var util = (‘Hello,Webpack’); console.log(util); })]从中可以看出开启 Scope Hoisting 后,函数申明由两个变成了一个,util.js 中定义的内容被直接注入到了 main.js 对应的模块中。 解决方案:webpack4 production mode会自动开启ModuleConcatenationPlugin,实现作用域提升。Tree Shakingtree shaking 是一个术语,通常用于描述移除 JavaScript 上下文中的未引用代码(dead-code)有的时候,代码里或者引用的模块里包含里一些没被使用的代码块,打包的时候也被打包到最终的文件里,增加了体积。这种时候,我们可以使用tree shaking技术来安全地删除文件中未使用的部分。使用方法:使用 ES2015 模块语法(即 import 和 export)。在项目 package.json 文件中,添加一个 “sideEffects” 属性。引入一个能够删除未引用代码(dead code)的压缩工具(minifier)(例如 UglifyJSPlugin)。分析工具在体积优化的路上,我们可以使用工具来分析我们打包出的体积最终优化成怎样的效果。常用的工具有两个:webpack-bundle-analyzer使用需要在webpack.config.js中配置plugins: [ new BundleAnalyzerPlugin()]执行完build后会打开网页,效果图如下:source-map-explorersource-map-explorer是根据source-map去分析出打包文件的体积大小,在本地调试是时设置 devtool: true,然后执行source-map-explorer build/static/js/main.js则可以去分析指定js的体积。效果图如下: ...

March 29, 2019 · 4 min · jiezi

从零开始开发一个react脚手架(一)

前沿: 脚手架工具一大堆,但如果全部用第三方的脚手架,项目做起来肯定束手束脚,想来点差异化的东西都很难,所以最好是整一份自己的脚手架工具,想咋玩咋玩。阅读了next脚手架和create-react-app脚手架源码,next脚手架太重,create-react-app好像没有服务器端渲染的内容,心血来潮自己写一个,中途会夹杂着两个脚手架工作的源码解读,只要理解了思想,就算照搬过来也是自己东西。哈哈(必然是会参考的)脚手架不难,但是涉及到源码解读,可能会分为几个章节,反正最后能实现create-react-app一模一样的效果,并且支持服务器端渲染。第一步 创建两个项目kkk-react 这个是脚手架项目,脚手架说白点就是node项目了,但要时时看效果,总不能一直publish到npm,所以需要npm link。cli-view,这个项目理论上应该是由kkk-react创建出来的,包含一些基本的文件和文件夹,并且package.json的scripts包含了start,build等构建命令。但因为是开发脚手架啊,这一步可以放到最后来弄,先把打包构建的步骤弄好。详细步骤 在kkk-react目录下, 执行npm init ,编辑package.json中的name为’kkk-react’,最后在项目根目录中 执行npm link命令。还有一些细节看截图更改package.json中的main,指向lib目录,开发阶段先这么玩,真正发布的时候,应该是新建一个bin字段,里面可以包含命令,当npm install这个脚手架的时候,这些命令生成对应的执行命令到node_modules的bin目录中,这样我们在项目中就执行了。开发的时候我们npm run dev,就能时时编译到lib目录了。至于cli-view一样的,先npm init,然后执行npm link kkk-react。创建一个cs.js引入kkk-react,就能看到效果了。可以看到引入后,通过node执行 就打印了我们在kkk-react输出的测试字段。同样的只是开发阶段这么玩,等一切都搞定了,就是通过npm run xxx,来执行对应的操作了。第一篇先这么着了,还只是试试水,争取明天出第二篇

March 26, 2019 · 1 min · jiezi

关于require.context的尝试

起因为什么会突然用到webpack这个管理特性呢?项目某个页面需要引入N张demo图片。即资源的批量引入:如果要引入10+个以上的图片资源,就需要写10+个如下的引入代码:import XXX from ‘relative/path/assets/imgs/xxx’;,那如果再多一点的静态资源需要引入呢?这时候require.context就派上了用场。文档官方文档的介绍先放在这里,可小觑一下,了解使用姿势。栗子????????来一个话不多说,针对上面的场景,我们上一下代码吧。场景需要我们引入某个指定文件夹下的所有webp格式的图片,在单击demo1的时候展示demo1下的x张案例图,在单击demoX的时候展示demoX下的x张案例图。 // 通过require.context的方式引入指定的路径下匹配到的模块引用 const demoImgsContext = require.context(’@src/assets/imgs/demo’, false, /.webp$/); … // 使用姿势 trigger(type) { this.demoImgs = Arry.from({length: config.type}, (key, value) => value + 1) .map(index => demoImgsContext(./${type}_demo${index}.webp)); }举一反三的场景还有需要的么?比如vuex引入多个module的store。也可以使用这个方法。 // 添加module文件是,文件命请按照module_XXX的方式命名 // 自动引入module文件夹下的js文件 const mutationContext = require.context(’./module’, false, /.*.js/); const modules = mutationContext.keys().reduce((prev, cur) => { // 排除module_root文件 const matches = cur.match(/module(?!.*root)(\w+).js/); const key = matches && matches[1]; key && (prev[key] = mutationContext(cur).default); return prev; }, {});接下来,让我们看一下,require.context是如何做到动态引入资源的呢?看下打包后的dist目录下,我们的静态图片案例chunk这个部分的代码是啥样的。trigger方法中引用模块资源的代码如下,对y方法进行调用,传入了一个资源的路径。 map(function (e) { return y("./".concat(t, “_demo”).concat(e, “.webp”)) })那y方法是什么呢?顺藤摸瓜,继续看下打包后的代码。 y = a(“ae36”);y方法是某个模块的export,继续查看这个id下的模块代码:ae36: function (t, e, a) { // 此处是一个map映射,key值和真正的资源id的映射 var i = { “./a_module_demo1.webp”: “6085”, “./a_module_demo2.webp”: “fd3b”, “./b_module_demo1.webp”: “cbf6”, “./b_module_demo2.webp”: “220e”, “./c_module_demo1.webp”: “273e”, “./c_module_demo2.webp”: “5a5e”, “./d_module_demo1.webp”: “75b0”, “./d_module_demo2.webp”: “2d3e” }; // 此处根据module的id值,真正require一个资源 function r(t) { var e = o(t); return a(e) } function o(t) { var e = i[t]; if (!(e + 1)) { var a = new Error(“Cannot find module ‘” + t + “’”); throw a.code = “MODULE_NOT_FOUND”, a } return e } r.keys = function () { return Object.keys(i) }, r.resolve = o, t.exports = r, r.id = “ae36” },“6085”,“fd3b"等map映射的value值可想而知,是真正的资源id值,其对应的模块映射如下:6085: function (t, e) { t.exports = “//${你配置的项目publicPath}/img/1_module_demo1.ed6db768.webp” },当用户触发trigger方法时,根据type和index指定的值,require.context存储的模块资源引用会根据key值找到真正的资源模块,进行require,浏览器会帮助我们下载相应的资源,做到了批量引入后按需加载的想法。手痒的同学可以磨刀霍霍试试了~ ...

March 25, 2019 · 1 min · jiezi

性能优化篇---Webpack构建代码质量压缩

Webpack构建速度优化基本优化完毕,接下来考虑的就是:线上代码质量的优化,即如何使用webpack构建出高质量的代码Webpack构建流程:初始化配置参数 -> 绑定事件钩子回调 -> 确定Entry逐一遍历 -> 使用loader编译文件 -> 输出文件提纲本次优化构建代码质量基本技术:reactRouter按需加载;公共代码提取,以及代码压缩;CDN接入;开启gzip压缩;接入treeShaking,剔除无用代码开启Scope Hoisting(生产环境代码构建)为实时查看每次配置后代码构建情况,使用Webpack监听文件避免每次手动build,并且开启webpack-jarvis,实时查看构建分析,npm i -D webpack-jarvis。开启监听模式watch: true,watchOptions: { ignored: /node_modules/, // 忽略监听文件 aggregateTimeout: 300, //文件变动后多久发起构建 poll: 1000, //每秒询问次数,越小越好}一、react-router4实现按需加载单页应用按需加载一般原则:将网站划分成一个个小功能,在按照每个功能的相关度将他们分成几个类;将没一个类合并成一个chunk,按需加载对应的代码;不可将用户首次进入网站时需要看到画面的对应功能Chunk按需加载;被分割出去的代码的加载需要一定的触发时机,即当用户操作了或者即将操作对应功能时再去加载对应的代码(默认使用react-router按需加载的触发条件是路由的变化)实现条件:使用插件:npm i react-loadable;配合bable插件npm i @babel/plugin-syntax-dynamic-import;代码示例:// .bablerc{ “plugins”: ["@babel/plugin-syntax-dynamic-import"]}// 示例代码Loadable({ loader: () => import(’./component’), //按需加载组件 loading: Loading, //处理组件加载的loading、error等 delay: 300 //延迟加载避免loading的闪烁问题});// Loading组件自定义// 接受三个props,其中pastDelay:等待时触发;timedOut:超时时触发超过delay;error:出错触发默认为200msconst Loading = ({ pastDelay, timedOut, error }) => { if (pastDelay) { return <Spin spinning tip=“Loadding…” ><div style={{height: 300}} /></Spin> } else if (timedOut) { return <Spin spinning tip=“Taking a long time…” ><div style={{height: 300}} /></Spin> } else if (error) { return <div>Error!</div>; } return null;};二、提取公共代码webpack.optimizationoptimization: { splitChunks: { chunks: “all”, cacheGroups: { vendors: { test: /node_modules/, name: ‘vendors’, minSize: 0, minChunks: 1, chunks: ‘initial’, priority: 2 // 该配置项是设置处理的优先级,数值越大越优先处理 }, commons: { name: “comomns”, test: resolve(“src/components”), // 可自定义拓展规则 minChunks: 2, // 最小共用次数 minSize:0, //代码最小多大,进行抽离 priority: 1, //该配置项是设置处理的优先级,数值越大越优先处理 } }}三、压缩文件js\css使用npm i -D webpack-parallel-uglify-plugin启用多线程并行压缩JSoptimization: { minimizer: [ new ParallelUglifyPlugin({ cacheDir: ‘.cache/’, //缓存压缩,默认不缓存,设置存放位置开启 test: /.js$/, //匹配需要压缩的文件,默认为/.js$/和Loader配置一样 //include: [], 使用正则去选择需要被压缩的文件和Loader配置一样 //exclude: [], 使用正则去去除不需要被压缩的文件和Loader配置一样 //workerCount: 2, 开启几个子进程并发执行压缩 // sourceMap: false, 是否输出source Map,开启会导致压缩变慢 // uglifyJS: {}, 用于压缩ES6代码不可和uglifyJS同时使用 uglifyJS:{//压缩ES5代码 output: { // 是否输出可读性较强的代码,即会保留空格和制表符,默认为输出,为了达到更好的压缩效果,可以设置为false beautify: false, //是否保留代码中的注释,默认为保留,为了达到更好的压缩效果,可以设置为false comments: false }, compress: { //是否在UglifyJS删除没有用到的代码时输出警告信息,默认为输出 warnings: false, //是否删除代码中所有的console语句,默认为不删除,开启后,会删除所有的console语句 drop_console: true, //是否内嵌虽然已经定义了,但是只用到一次的变量,比如将 var x = 1; y = x, 转换成 y = 1, 默认为否 collapse_vars: true, // 提取出现多次但是没有定义成变量去引用的静态值 reduce_vars:true } }, }), ]},提取和压缩Css使用插件:optimize-css-assets-webpack-plugin、mini-css-extract-plugin使用示例:// 提取css到单独的文件const MiniCssExtractPlugin = require(“mini-css-extract-plugin”);// optimizeCssPlugin CSS文件压缩插件const optimizeCssPlugin = require(‘optimize-css-assets-webpack-plugin’);const extractSCSS = new MiniCssExtractPlugin({ filename: ‘css/[name].[contenthash:8].css’, chunkFilename: ‘css/[name]_[contenthash:8].css’, fallback:‘style-loader’});……plugins: [ new optimizeCssPlugin({ assetNameRegExp: /.css$/g, cssProcessor: require(‘cssnano’), cssProcessorOptions: { discardComments: { removeAll: true } }, canPrint: true }),]webpack配置接入CDNCDN网站接入CDN,需要将网页的静态资源上传到CDN服务器,使用CDN地址访问;使用CDN可以决解资源并行下载限制,处理静态资源Cookie同域名携带等问题;CDN缓存和回源需要合理的设置静态资源hash接入CDN会引入多个域名,增加域名解析时间,可进行预解析域名<link rel=“dns-prefetch” href="//js.dns.com" />webpack实现接入output.publicPath设置JavaScript地址css-loader.publicPath设置CSS导入的资源地址WebPlugin.stylePublicPath中设置Css文件地址// JavaScriptoutput: { publicPath: ‘//js.cdn.com/js/’, path: path.join(__dirname, ‘../docs/dist’), // 打包后的文件存放的地方 // 为输出的JavaScript文件名加上Hash值使用chunkhash(chunkhash:根据模块内容变化;hash: 根据每次构建随机) filename: “js/[name].[chunkhash:8].js”, chunkFilename: “js/[name]-[id].[chunkhash:8].js”,},开启gzip压缩使用插件:npm i -D compression-webpack-plugin;webpack配置const CompressionPlugin = require(“compression-webpack-plugin”);plugins: [ new CompressionPlugin({ filename: ‘[path].gz[query]’, //目标资源名称。[file] 会被替换成原资源。[path] 会被替换成原资源路径,[query] 替换成原查询字符串 algorithm: ‘gzip’,//算法 test: /.(js|css)$/, //压缩 js 与 css threshold: 10240,//只处理比这个值大的资源。按字节计算 minRatio: 0.8//只有压缩率比这个值小的资源才会被处理 })]后台开启使用koaconst staticCache = require(‘koa-static-cache’);import config from ‘./configs’;const app = new Koa();app.use(staticCache(path.resolve(__dirname, “../dist”), { maxAge: 7 * 24 * 60 * 60, gzip: true, //开启 dynamic: true,}))接入treeShaking,剔除无用代码Tree Shaking可以用来找出有用代码,去除JavaScript中用不上的死代码;但是它依赖于ES6静态花模块语法import\export的导入和导出webpack接入修改.babelrc保留ES6模块话语句注意新版本babel-preset-env已经预设babel-preset-es2015,babel推荐使用babel-preset-env取代babel-preset-es2015,并且继续使用babel-preset-es2015会发出警告信息。{ “presets”: [ [“env”, { “modules”: false }] ], “plugins”: [“syntax-dynamic-import”]}webpack –display-used-exports运行构建带上–display-used-exports可追踪到Tree Shaking的工作;Webpack只能正确的分析出如何剔除死代码,需要接入UglifyJs处理剔除(配置见上)开启Scope Hoistionscope hoisting即作用域提升;在构建过程中,webpack会借助ES6 模块化的静态特性,确定模块的依赖关系,将一个bundle中的静态依赖提升到顶部。(所以需要和接入treeShaking一样配置Babel开启ES6模块化)原理:分析模块间的依赖关系,尽可能的将零散的模块合并到一个函数中去,前提不能造成代码冗余,因此只有被引用了一次的模块才能被合并。接入好处:代码体积减少代码在运行时因为创建的函数作用域更少了,内存开销也随之变小webpack接入ModuleConcatenationPlugin内置插件const ModuleConcatPlugin = require(‘webpack/lib/optimize/ModuleConcatenationPlugin’);plugins: [ new ModuleConcatPlugin(), //开启scope Hoisting ],“积跬步、行千里”—— 持续更新中~,喜欢的话留下个赞和关注哦!往期经典好文:你不知道的CORS跨域资源共享性能优化篇—Webpack构建速度优化React组件库封装初探–Modal使用pm2部署node生产环境 ...

March 25, 2019 · 2 min · jiezi

Vue@2.6+webpack@4.29.6 打造TODO应用

demo地址:欢迎startdemo截图:简介:此项目首先完成一个前端工程的配置,通过webpack搭建一个完善的vue的workflow,然后基于Vue实现TODOList的简单功能,并涵盖了vue的.vue文件以及jsx的开发模式的了解。使用方法:下载源码git clone https://github.com/1020196987…安装npm依赖npm install进入项目目录&&运行项目npm run start由于安装了webpack-dev-server浏览器会自动打开

March 18, 2019 · 1 min · jiezi

从零开始的Webpack4教程

1、了解Webpack相关什么是webpackWebpack是一个模块打包器(bundler)。在Webpack看来, 前端的所有资源文件(js/json/css/img/less/…)都会作为模块处理它将根据模块的依赖关系进行静态分析,生成对应的静态资源五个核心概念Entry:入口起点(entry point)指示 webpack 应该使用哪个模块,来作为构建其内部依赖图的开始。Output:output 属性告诉 webpack 在哪里输出它所创建的 bundles,以及如何命名这些文件,默认值为 ./dist。Loader:loader 让 webpack 能够去处理那些非 JavaScript 文件(webpack 自身只能解析 JavaScript)。Plugins:插件则可以用于执行范围更广的任务。插件的范围包括,从打包优化和压缩,一直到重新定义环境中的变量等。Mode:模式,有生产模式production和开发模式development理解LoaderWebpack 本身只能加载JS/JSON模块,如果要加载其他类型的文件(模块),就需要使用对应的loader 进行转换/加载Loader 本身也是运行在 node.js 环境中的 JavaScript 模块它本身是一个函数,接受源文件作为参数,返回转换的结果loader 一般以 xxx-loader 的方式命名,xxx 代表了这个 loader 要做的转换功能,比如 json-loader。理解Plugins插件可以完成一些loader不能完成的功能。插件的使用一般是在 webpack 的配置信息 plugins 选项中指定。配置文件(默认)webpack.config.js : 是一个node模块,返回一个 json 格式的配置信息对象2、开启项目初始化项目:生成package.json文件{ “name”: “webpack_test”, “version”: “1.0.0”} 安装webpacknpm install webpack webpack-cli -g //全局安装npm install webpack webpack-cli -D //本地安装3、编译打包应用创建js文件src/js/app.jssrc/js/module1.jssrc/js/module2.jssrc/js/module3.js创建json文件src/json/data.json创建主页面:src/index.html运行指令开发配置指令:webpack src/js/app.js -o dist/js/app.js –mode=development功能: webpack能够编译打包js和json文件,并且能将es6的模块化语法转换成浏览器能识别的语法生产配置指令:webpack src/js/app.js -o dist/js/app.js –mode=production功能: 在开发配置功能上加上一个压缩代码结论:webpack能够编译打包js和json文件能将es6的模块化语法转换成浏览器能识别的语法能压缩代码缺点:不能编译打包css、img等文件不能将js的es6基本语法转化为es5以下语法改善:使用webpack配置文件解决,自定义功能4、使用webpack配置文件目的:在项目根目录定义配置文件,通过自定义配置文件,还原以上功能文件名称:webpack.config.js文件内容:const {resolve} = require(‘path’); //node内置核心模块,用来设置路径。module.exports = { entry: ‘./src/js/app.js’, // 入口文件 output: { // 输出配置 filename: ‘./js/bundle.js’, // 输出文件名 path: resolve(__dirname, ‘dist’) //输出文件路径配置 }, mode: ‘development’ //开发环境(二选一) mode: ‘production’ //生产环境(二选一)};运行指令: webpack5、js语法检查安装loadernpm install eslint-loader eslint –save-dev配置loadermodule: { rules: [ { test: /.js$/, //只检测js文件 exclude: /node_modules/, //排除node_modules文件夹 enforce: “pre”, //提前加载使用 use: { //使用eslint-loader解析 loader: “eslint-loader” } } ]}修改package.json(需要删除注释才能生效)“eslintConfig”: { //eslint配置 “parserOptions”: { “ecmaVersion”: 8, // es8 “sourceType”: “module”, // ECMAScript 模块 }}运行指令:webpack6、js语法转换安装loadernpm install babel-loader @babel/core @babel/preset-env –save-dev配置loadermodule: { rules: [ { oneOf: [ //数组中的配置只有一个能够生效, 后面的配置都会放在当前数组中 { test: /.js$/, exclude: /node_modules/, use: { loader: “babel-loader”, options: { presets: [’@babel/preset-env’] } } } ] } ]}运行指令:webpack7、打包less资源创建less文件src/less/test1.lesssrc/less/test2.less入口app.js文件引入less资源安装loadernpm install css-loader style-loader less-loader less –save-dev配置loaderoneOf: [ { test: /.less$/, use: [{ loader: “style-loader” }, { loader: “css-loader” }, { loader: “less-loader” }] }]运行指令:webpack在index.html中引入打包生成的dist/js/bundle.js文件,观察效果8、打包样式文件中的图片资源添加2张图片:小图, 小于8kb: src/images/1.png大图, 大于8kb: src/images/2.jpg在less文件中通过背景图的方式引入图片安装loadernpm install file-loader url-loader –save-dev补充:url-loader是对象file-loader的上层封装,使用时需配合file-loader使用。配置loader{ test: /.(png|jpg|gif|svg)$/, use: [ { loader: ‘url-loader’, options: { outputPath: ‘images/’, //在output基础上,修改输出图片文件的位置 publicPath: ‘../dist/images/’, //修改背景图引入url的路径 limit: 8 * 1024, // 8kb大小以下的图片文件都用base64处理 name: ‘[hash:8].[ext]’ // hash值为7位,ext自动补全文件扩展名 } } ]}运行指令:webpack在index.html中引入打包生成的dist/js/bundle.js文件,观察效果9、打包html文件添加html文件src/index.html注意不要在html中引入任何css和js文件安装插件Pluginsnpm install clean-webpack-plugin –save-dev在webpack.config.js中引入插件(插件都需要手动引入,而loader会自动加载)const CleanWebpackPlugin = require(‘clean-webpack-plugin’)配置插件Pluginsplugins: [ new HtmlWebpackPlugin({ template: ‘./src/index.html’ }),]运行指令:webpack10、打包html中图片资源添加图片在src/index.html添加两个img标签安装loadernpm install html-loader –save-dev修改entryentry: [’./src/js/app.js’, ‘./src/index.html’]配置loader{ test: /.(html)$/, use: { loader: ‘html-loader’ }}运行指令:webpack11、打包其他资源添加字体文件src/media/iconfont.eotsrc/media/iconfont.svgsrc/media/iconfont.ttfsrc/media/iconfont.woffsrc/media/iconfont.woff2修改样式@font-face { font-family: ‘iconfont’; src: url(’../media/iconfont.eot’); src: url(’../media/iconfont.eot?#iefix’) format(’embedded-opentype’), url(’../media/iconfont.woff2’) format(‘woff2’), url(’../media/iconfont.woff’) format(‘woff’), url(’../media/iconfont.ttf’) format(’truetype’), url(’../media/iconfont.svg#iconfont’) format(‘svg’);}.iconfont { font-family: “iconfont” !important; font-size: 16px; font-style: normal; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale;}修改html,添加字体配置loader{ loader: ‘file-loader’, exclude: [/.js$/, /.html$/, /.json$/], options: { outputPath: ‘media/’, publicPath: ‘../dist/media/’, name: ‘[hash:8].[ext]’, },}运行指令:webpack12、自动编译打包运行安装loadernpm install webpack-dev-server –save-dev引入webpackconst webpack = require(‘webpack’);修改webpack配置对象(注意不是loader中)devtool: ‘inline-source-map’, // 将编译后的代码映射回原始源代码,更容易地追踪错误和警告devServer: { contentBase: ‘./dist’, //项目根路径 hot: true, //开启热模替换功能 open: true //自动打开浏览器},plugins: [ new webpack.NamedModulesPlugin(), new webpack.HotModuleReplacementPlugin()]修改loader部分配置因为构建工具以dist为根目录,不用再找dist了publicPath: ‘../dist/images/’ –> publicPath: ‘images/‘publicPath: ‘../dist/media/’ –> publicPath: ‘media/‘修改package.json中scripts指令"start": “webpack-dev-server”,“dev”: “webpack-dev-server"运行指令:npm start / npm run dev以上就是webpack开发环境的配置,可以在内存中自动打包所有类型文件并有自动编译运行、热更新等功能。13、准备生产环境创建文件夹config,将webpack.config.js复制两份webpack.dev.jswebpack.prod.js修改webpack.prod.js配置,删除webpack-dev-server配置module.exports = { output: { filename: ‘js/[name].[hash:8].js’, //添加了hash值, 实现静态资源的长期缓存 publicPath: ‘/’ //所有输出资源公共路径 }, module: { //loader其他没有变化,只放了变化部分,只有需要修改路径部分改了 rules: [ { oneOf: [ { test: /.(png|jpg|gif|svg)$/, use: [ { loader: ‘url-loader’, options: { limit: 8 * 1024, // 8kb大小以下的图片文件都用base64处理 name: ‘images/[name].[hash:8].[ext]’ } } ] }, { loader: ‘file-loader’, exclude: [/.js$/, /.html$/, /.json$/], options: { name: ‘media/[name].[hash:8].[ext]’, }, } ] } ] }, mode: ‘production’, //修改为生产环境}修改package.json的指令"start”: “webpack-dev-server –config ./config/webpack.dev.js"“dev”: “webpack-dev-server –config ./config/webpack.dev.js"“build”: “webpack –config ./config/webpack.prod.js"开发环境指令npm startnpm run dev生产环境指令npm run build注意: 生产环境代码需要部署到服务器上才能运行npm i serve -gserve -s dist14、清除打包文件目录安装插件npm install clean-webpack-plugin –save-dev引入插件const CleanWebpackPlugin = require(‘clean-webpack-plugin’);配置插件new CleanWebpackPlugin()运行指令:npm run build15、提取css成单独文件安装插件npm install mini-css-extract-plugin –save-dev引入插件const MiniCssExtractPlugin = require(“mini-css-extract-plugin”);配置loader{ test: /.less$/, use: [ MiniCssExtractPlugin.loader, ‘css-loader’, ’less-loader’, ]}配置插件new MiniCssExtractPlugin({ filename: “css/[name].[hash:8].css”, chunkFilename: “css/[id].[hash:8].css”})运行指令npm run buildserve -s dist16、添加css兼容安装loadernpm install postcss-loader autoprefixer –save-dev配置loader{ test: /.less$/, use: [ MiniCssExtractPlugin.loader, ‘css-loader’, ‘postcss-loader’, ’less-loader’, ]}在项目根目录添加postcss配置文件:postcss.config.jsmodule.exports = { “plugins”: { “autoprefixer”: { “browsers”: [ “ie >= 8”, “ff >= 30”, “chrome >= 34”, “safari >= 8”, “opera >= 23” ] } }}运行指令:npm run buildserve -s dist17、压缩css安装插件npm install optimize-css-assets-webpack-plugin –save-dev引入插件const OptimizeCssAssetsPlugin = require(‘optimize-css-assets-webpack-plugin’);配置插件new OptimizeCssAssetsPlugin({ cssProcessorPluginOptions: { preset: [‘default’, { discardComments: { removeAll: true } }], },})运行指令:npm run buildserve -s dist18、图片压缩安装loadernpm install img-loader imagemin-gifsicle imagemin-mozjpeg imagemin-pngquant imagemin-svgo imagemin –save-dev配置loader{ test: /.(png|jpg|gif|svg)$/, use: [ { loader: ‘url-loader’, options: { limit: 8 * 1024, // 8kb大小以下的图片文件都用base64处理 name: ‘images/[name].[hash:8].[ext]’ } }, { loader: ‘img-loader’, options: { plugins: [ require(‘imagemin-gifsicle’)({ interlaced: false }), require(‘imagemin-mozjpeg’)({ progressive: true, arithmetic: false }), require(‘imagemin-pngquant’)({ floyd: 0.5, speed: 2 }), require(‘imagemin-svgo’)({ plugins: [ { removeTitle: true }, { convertPathData: false } ] }) ] } } ]}运行指令:npm run buildserve -s dist19、压缩html修改插件配置new HtmlWebpackPlugin({ template: ‘./src/index.html’, minify: { removeComments: true, collapseWhitespace: true, removeRedundantAttributes: true, useShortDoctype: true, removeEmptyAttributes: true, removeStyleLinkTypeAttributes: true, keepClosingSlash: true, minifyJS: true, minifyCSS: true, minifyURLs: true, }})运行指令:npm run buildserve -s dist以上就是webpack生产环境的配置,可以生成打包后的文件。到这里基本配置已经告一段落了,所有配置我已经放在 仓库 中了。后期会考虑出一期优化配置,将webpack优化到底~ ...

March 17, 2019 · 3 min · jiezi

性能优化篇---Webpack构建速度优化

如何输出Webpack构建分析输出Webpack构建信息的.json文件:webpack –profile –json > starts.json–profile:记录构建中的耗时信息–json:以json格式输出构建结果,最后只输出一个json文件(包含所有的构建信息)web可视化查看构建分析:得到了webpack构建信息文件starts.json,如何进行很好的可视化查看?方案一:通过可视化分析工具Webpack Analyse,是个在线Web应用,上传starts.json文件就可以;不过好像需要翻墙;方案二:安装webpack-bundle-analyzer工具npm i -g webpack-bundle-analyzer,生成starts.json后直接在其文件夹目录执行webpack-bundle-analyzer后,浏览器会打开对应网页并展示构建分析文档地址webpack-bundle-analyzerwebpack-dashboard是一款统计和优化webpack日志的工具,可以以表格形势展示日志信息。其中包括构建过程和状态、日志以及涉及的模块列表jarvis是一款基于webapck-dashboard的webpack性能分析插件,性能分析的结果在浏览器显示,比webpack-bundler-anazlyer更美观清晰GitHub文档地址npm i -D webpack-jarviswebpack.config.js配置:const Jarvis = require(“webpack-jarvis”);plugins: [ new Jarvis({ watchOnly: false, port: 3001 // optional: set a port })];port:监听的端口,默认1337,监听面板将监听这个端口,通常像http://localhost:port/host:域名,默认localhost,不限制域名。watchOnly:仅仅监听编译阶段。默认为true,如果高为false,jarvis不仅仅运行在编译阶段,在编译完成后还保持运行状态。界面:看到构建时间为:Time: 11593ms(作为优化时间对比)webpack配置优化webpack在启动时会从配置的Entry出发,解析出文件中的导入语句,再递归解析。对于导入语句Webpack会做出以下操作:根据导入语句寻找对应的要导入的文件;在根据要导入的文件后缀,使用配置中的Loader去处理文件(如使用ES6需要使用babel-loader处理)针对这两点可以优化查找途径优化Loader配置Loader处理文件的转换操作是很耗时的,所以需要让尽可能少的文件被Loader处理{ test: /.js$/, use: [ ‘babel-loader?cacheDirectory’,//开启转换结果缓存 ], include: path.resolve(__dirname, ‘src’),//只对src目录中文件采用babel-loader exclude: path.resolve(__dirname,’ ./node_modules’),//排除node_modules目录下的文件},优化resolve.modules配置resolve.modules用于配置webpack去哪些目录下寻找第三方模块,默认是[’node_modules’],但是,它会先去当前目录的./node_modules查找,没有的话再去../node_modules最后到根目录;所以当安装的第三方模块都放在项目根目录时,就没有必要安默认的一层一层的查找,直接指明存放的绝对位置resolve: { modules: [path.resolve(__dirname, ’node_modules’)], }优化resolve.extensions配置在导入没带文件后缀的路径时,webpack会自动带上后缀去尝试询问文件是否存在,而resolve.extensions用于配置尝试后缀列表;默认为extensions:[‘js’,‘json’];及当遇到require(’./data’)时webpack会先尝试寻找data.js,没有再去找data.json;如果列表越长,或者正确的后缀越往后,尝试的次数就会越多;所以在配置时为提升构建优化需遵守:频率出现高的文件后缀优先放在前面;列表尽可能的小;书写导入语句时,尽量写上后缀名因为项目中用的jsx较多,所以配置extensions: [".jsx",".js"],基本配置后查看构建速度:Time: 10654ms;配置前为Time: 11593ms使用DllPlugin优化在使用webpack进行打包时候,对于依赖的第三方库,如react,react-dom等这些不会修改的依赖,可以让它和业务代码分开打包;只要不升级依赖库版本,之后webpack就只需要打包项目业务代码,遇到需要导入的模块在某个动态链接库中时,就直接去其中获取;而不用再去编译第三方库,这样第三方库就只需要打包一次。接入需要完成的事:将依赖的第三方模块抽离,打包到一个个单独的动态链接库中当需要导入的模块存在动态链接库中时,让其直接从链接库中获取项目依赖的所有动态链接库都需要被加载接入工具(webpack已内置)DllPlugin插件:用于打包出一个个单独的动态链接库文件;DllReferencePlugin:用于在主要的配置文件中引入DllPlugin插件打包好的动态链接库文件配置webpack_dll.config.js构建动态链接库const path = require(‘path’);const DllPlugin = require(‘webpack/lib/DllPlugin’);module.exports = { mode: ‘production’, entry: { // 将React相关模块放入一个动态链接库 react: [‘react’,‘react-dom’,‘react-router-dom’,‘react-loadable’], librarys: [‘wangeditor’], utils: [‘axios’,‘js-cookie’] }, output: { filename: ‘[name]-dll.js’, path: path.resolve(__dirname, ‘dll’), // 存放动态链接库的全局变量名,加上_dll_防止全局变量冲突 library: ‘dll[name]’ }, // 动态链接库的全局变量名称,需要可output.library中保持一致,也是输出的manifest.json文件中name的字段值 // 如react.manifest.json字段中存在"name":"_dll_react" plugins: [ new DllPlugin({ name: ‘dll[name]’, path: path.join(__dirname, ‘dll’, ‘[name].manifest.json’) }) ]}webpack.pro.config.js中使用const DllReferencePlugin = require(‘webpack/lib/DllReferencePlugin’);…plugins: [ // 告诉webpack使用了哪些动态链接库 new DllReferencePlugin({ manifest: require(’./dll/react.manifest.json’) }), new DllReferencePlugin({ manifest: require(’./dll/librarys.manifest.json’) }), new DllReferencePlugin({ manifest: require(’./dll/utils.manifest.json’) }),]注意:在webpack_dll.config.js文件中,DllPlugin中的name参数必须和output.library中的一致;因为DllPlugin的name参数影响输出的manifest.json的name;而webpack.pro.config.js中的DllReferencePlugin会读取manifest.json的name,将值作为从全局变量中获取动态链接库内容时的全局变量名执行构建webpack –progress –colors –config ./webpack.dll.config.jswebpack –progress –colors –config ./webpack.prod.jshtml中引入dll.js文件构建时间对比:[“11593ms”,“10654ms”,“8334ms”]HappyPack并行构建优化核心原理:将webpack中最耗时的loader文件转换操作任务,分解到多个进程中并行处理,从而减少构建时间。HappyPack接入HappyPack安装:npm i -D happypack重新配置rules部分,将loader交给happypack来分配:const HappyPack = require(‘happypack’);const happyThreadPool = HappyPack.ThreadPool({size: 5}); //构建共享进程池,包含5个进程…plugins: [ // happypack并行处理 new HappyPack({ // 用唯一ID来代表当前HappyPack是用来处理一类特定文件的,与rules中的use对应 id: ‘babel’, loaders: [‘babel-loader?cacheDirectory’],//默认设置loader处理 threadPool: happyThreadPool,//使用共享池处理 }), new HappyPack({ // 用唯一ID来代表当前HappyPack是用来处理一类特定文件的,与rules中的use对应 id: ‘css’, loaders: [ ‘css-loader’, ‘postcss-loader’, ‘sass-loader’], threadPool: happyThreadPool })],module: { rules: [ { test: /.(js|jsx)$/, use: [‘happypack/loader?id=babel’], exclude: path.resolve(__dirname,’ ./node_modules’), }, { test: /.(scss|css)$/, //使用的mini-css-extract-plugin提取css此处,如果放在上面会出错 use: [MiniCssExtractPlugin.loader,‘happypack/loader?id=css’], include:[ path.resolve(__dirname,‘src’), path.join(__dirname, ‘./node_modules/antd’) ] },}参数:threads:代表开启几个子进程去处理这一类文件,默认是3个;verbose:是否运行HappyPack输出日志,默认true;threadPool:代表共享进程池,即多个HappyPack示例使用一个共享进程池中的子进程去处理任务,以防资源占有过多代码压缩用ParallelUglifyPlugin代替自带的 UglifyJsPlugin插件自带的JS压缩插件是单线程执行的,而webpack-parallel-uglify-plugin可以并行的执行配置参数:uglifyJS: {}:用于压缩 ES5 代码时的配置,Object 类型test: /.js$/g:使用正则去匹配哪些文件需要被 ParallelUglifyPlugin 压缩,默认是 /.js$/include: []:使用正则去包含被压缩的文件,默认为 [].exclude: []: 使用正则去包含不被压缩的文件,默认为 []cacheDir: ‘’:缓存压缩后的结果,下次遇到一样的输入时直接从缓存中获取压缩后的结果并返回,默认不会缓存,开启缓存设置一个目录路径workerCount: ‘’:开启几个子进程去并发的执行压缩。默认是当前运行电脑的 CPU 核数减去1sourceMap: false:是否为压缩后的代码生成对应的Source Map, 默认不生成…minimizer: [ // webpack:production模式默认有配有js压缩,但是如果这里设置了css压缩,js压缩也要重新设置,因为使用minimizer会自动取消webpack的默认配置 new optimizeCssPlugin({ assetNameRegExp: /.css$/g, cssProcessor: require(‘cssnano’), cssProcessorOptions: { discardComments: { removeAll: true } }, canPrint: true }), new ParallelUglifyPlugin({ cacheDir: ‘.cache/’, uglifyJS:{ output: { // 是否输出可读性较强的代码,即会保留空格和制表符,默认为输出,为了达到更好的压缩效果,可以设置为false beautify: false, //是否保留代码中的注释,默认为保留,为了达到更好的压缩效果,可以设置为false comments: false }, compress: { //是否在UglifyJS删除没有用到的代码时输出警告信息,默认为输出 warnings: false, //是否删除代码中所有的console语句,默认为不删除,开启后,会删除所有的console语句 drop_console: true, //是否内嵌虽然已经定义了,但是只用到一次的变量,比如将 var x = 1; y = x, 转换成 y = 1, 默认为否 collapse_vars: true, } }}),]构建结果对比:[“11593ms”,“10654ms”,“8334ms”,“7734ms”]整体构建速度从12000ms降到现在的8000ms“积跬步、行千里”—— 持续更新中~,喜欢的话留下个赞和关注哦!往期经典好文:你不知道的CORS跨域资源共享Koa日志中间件封装开发(log4js)团队合作必备的Git操作使用pm2部署node生产环境 ...

March 13, 2019 · 2 min · jiezi

vue init webpack-simple project报错

今天在创建vue简单模板时遇到了报错:vue init webpack-simple project报错为:vue-cli ¡¤ Failed to download repo vuejs-templates/webpack-simple: getaddrinfo ENOTFOUND codeload.github.com codeload.github.com:443原因是电脑的hosts文件被修改了,hosts文件的位置在:C:\Windows\System32\etc\hosts打开后把除了默认的配置都删除,就可以成功创建项目了。

March 13, 2019 · 1 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

造轮子进阶--webpack4&vue2

cssnpm i css-loader style-loader –save-dev在webpack.config.js 中增加如下配置:rules: [{ test: /.css$/, use: [ ‘style-loader’, ‘css-loader’ // 执行顺序是从右往左 ]}创建css文件,并引入,打包后可以正常解析,但是css文件是混入js中,所以需要分离css.分离csswebpack4中完成这个功能的插件是mini-css-extract-plugin.npm install mini-css-extract-plugin –save-dev在webpack.prod.js 中增加如下配置:const MiniCssExtractPlugin = require(‘mini-css-extract-plugin’);module.exports = { module: { rules: [ { test: /.css$/, use: [ { loader: MiniCssExtractPlugin.loader }, ‘css-loader’ ] } ] }, plugins:[ new MiniCssExtractPlugin({ filename: ‘[name].css’ }) ]}处理图片新建文件夹src/images,用来放图片。file-loadernpm i file-loader –save-dev{ test: /.(png|jpg|gif|svg)$/, loader: ‘file-loader’, options: { name: ‘[name].[ext]?[hash]’ }}url-loadernpm i url-loader –save-dev区别file-loader:解决引用路径的问题,拿background样式用url引入背景图来说,我们都知道,webpack最终会将各个模块打包成一个文件,因此我们样式中的url路径是相对入口html页面的,而不是相对于原始css文件所在的路径的。这就会导致图片引入失败。这个问题是用file-loader解决的,file-loader可以解析项目中的url引入(不仅限于css),根据我们的配置,将图片拷贝到相应的路径,再根据我们的配置,修改打包后文件引用路径,使之指向正确的文件。url-loader:如果图片较多,会发很多http请求,会降低页面性能。这个问题可以通过url-loader解决。url-loader会将引入的图片编码,生成dataURl。相当于把图片数据翻译成一串字符。再把这串字符打包到文件中,最终只需要引入这个文件就能访问图片了。当然,如果图片较大,编码会消耗性能。因此url-loader提供了一个limit参数,小于limit字节的文件会被转为DataURl,大于limit的还会使用file-loader进行copy。url-loader和file-loader是什么关系呢?简答地说,url-loader封装了file-loader。url-loader不依赖于file-loader,即使用url-loader时,只需要安装url-loader即可,不需要安装file-loader,因为url-loader内置了file-loader。通过上面的介绍,我们可以看到,url-loader工作分两种情况:1.文件大小小于limit参数,url-loader将会把文件转为DataURL;2.文件大小大于limit,url-loader会调用file-loader进行处理,参数也会直接传给file-loader。因此我们只需要安装url-loader即可。扩展—vue中的图片动态引用img标签<template> <img :src=“imgUrl”></template>第一种:import imgUrl from “../assets/test.png”;第二种:data() { return { imgUrl: require("../assets/test.png") }}第三种:created() { this.imgUrl = require("@/" + urlTemp);}css中url公司项目中有个需求,就是每个模块都要展示介绍功能的图片,切模块时,图片要相应的改变,是通过background来实现的,这个时候就需要动态引用图片。实现方式如下:<div :style="{background: ‘url(’ + require(’../assets/’ + imgUrl[i]) +’)’}"></div>data() { return { imgUrl: [ “test1.png”, “test2.png”, “test3.png”, ] }}或者直接将图片放入static文件夹中,也可以通过循环拿到。然后,我试验了一下,除了以上两种方式,其他方式写的都报错。不知道有没有大佬指点一下~~下面的只是记录一下,跟上面的无关,<div :style=“style”></div>data() { return { style: [ {background: ‘url(’ + require(’../assets/test1.png’) +’)’} // 不能写变量 ] }}处理js文件参考链接:webpack4.0各个击破(4)—— Javascript & splitChunk代码分离在webpack4中删除了原来的CommonsChunkPlugin插件,内部集成的optimization.splitChunks选项可以直接进行代码分离。在webpack.prod.js 中增加如下配置:optimization: { splitChunks: { chunks: ‘async’, //默认只作用于异步模块,为all时对所有模块生效,initial对同步模块有效 minSize: 30000, //合并前模块文件的体积 minChunks: 1 //最少被引用次数 }} ...

March 12, 2019 · 1 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 错误

Entrypoint undefined = index.html 在配置中文件中module.exports对象中添加stats: { children: false } module.exports = { stats: { children: false } }

March 11, 2019 · 1 min · jiezi

使用 Webpack 与 Babel 配置 ES6 开发环境

使用 Webpack 与 Babel 配置 ES6 开发环境安装 Webpack安装:# 本地安装$ npm install –save-dev webpack webpack-cli# 全局安装$ npm install -g webpack webpack-cli在项目根目录下新建一个配置文件—— webpack.config.js 文件:const path = require(‘path’);module.exports = { mode: ’none’, entry: ‘./src/index.js’, output: { filename: ‘bundle.js’, path: path.resolve(__dirname, ‘dist’) }}在 src 目录下新建 a.js 文件:export const isNull = val => val === nullexport const unique = arr => […new Set(arr)]在 src 目录下新建 index.js 文件:import { isNull, unique } from ‘./a.js’const arr = [1, 1, 2, 3]console.log(unique(arr))console.log(isNull(arr))执行编译打包命令,完成后打开 bundle.js 文件发现 isNull 和 unique 两个函数没有被编译,和 webpack 官方说法一致:webpack 默认支持 ES6 模块语法,要编译 ES6 代码依然需要 babel 编译器。安装配置 Babel 编译器使用 Babel 必须先安装 @babel/core 和 @babel/preset-env 两个模块,其中 @babel/core 是 Babel 的核心存在,Babel 的核心 api 都在这个模块里面,比如:transform。而 @babel/preset-env 是一个智能预设,允许您使用最新的 JavaScript,而无需微观管理您的目标环境需要哪些语法转换(以及可选的浏览器polyfill)。因为这里使用的打包工具是 Webpack,所以还需要安装 babel-loader 插件。安装:$ npm install –save-dev @babel/core @babel/preset-env babel-loader新建 .babelrc 文件:{ “presets”: [ “@babel/preset-env” ]}修改 webpack 配置文件(webpack.config.js):const path = require(‘path’);module.exports = { mode: ’none’, entry: ‘./src/index.js’, output: { filename: ‘bundle.js’, path: path.resolve(__dirname, ‘dist’) }, module: { rules: [ { test: /.js$/, loader: ‘babel-loader’, exclude: /node_modules/ } ] }}由于 babel 默认只转换 ES6 新语法,不转换新的 API,如:Set、Map、Promise等,所以需要安装 @babel/polyfill 转换新 API。安装 @babel/plugin-transform-runtime 优化代码,@babel/plugin-transform-runtime 是一个可以重复使用 Babel 注入的帮助程序代码来节省代码的插件。安装 @babel/polyfill、@babel/plugin-transform-runtime 两个插件:$ npm install –save-dev @babel/polyfill @babel/plugin-transform-runtime修改 .babelrc 配置文件:{ “presets”: [ ["@babel/preset-env", { “useBuiltIns”: “usage”, // 在每个文件中使用polyfill时,为polyfill添加特定导入。利用捆绑器只加载一次相同的polyfill。 “modules”: false // 启用将ES6模块语法转换为其他模块类型,设置为false不会转换模块。 }] ], “plugins”: [ ["@babel/plugin-transform-runtime", { “helpers”: false }] ]}最后,配置兼容的浏览器环境。在 .babelrc 配置文件中设置 targets 属性:{ “presets”: [ ["@babel/preset-env", { “useBuiltIns”: “usage”, “modules”: false, “targets”: { “browsers”: “last 2 versions, not ie <= 9” } }] ], “plugins”: [ ["@babel/plugin-transform-runtime", { “helpers”: false }] ]}执行命令编译代码,完成后检查 bundle.js 文件,是否成功转换新 API 。如果发现以下代码即说明转换成功:// 23.2 Set Objectsmodule.exports = webpack_require(80)(SET, function (get) { return function Set() { return get(this, arguments.length > 0 ? arguments[0] : undefined); };}, { // 23.2.3.1 Set.prototype.add(value) add: function add(value) { return strong.def(validate(this, SET), value = value === 0 ? 0 : value, value); }}, strong);其他关于 js 压缩和 Webpack 启用 tree shaking 功能的设置本文不在赘述。配置文件详情概览package.json 文件:{ “name”: “demo”, “version”: “1.0.0”, “description”: “”, “main”: “index.js”, “scripts”: { “test”: “echo "Error: no test specified" && exit 1”, “dev”: “webpack” }, “keywords”: [], “author”: “”, “license”: “ISC”, “devDependencies”: { “@babel/core”: “^7.3.4”, “@babel/plugin-transform-runtime”: “^7.3.4”, “@babel/polyfill”: “^7.2.5”, “@babel/preset-env”: “^7.3.4”, “babel-loader”: “^8.0.5”, “webpack”: “^4.29.6”, “webpack-cli”: “^3.2.3” }}webpack.config.js 文件:const path = require(‘path’);module.exports = { mode: ’none’, entry: ‘./src/index.js’, output: { filename: ‘bundle.js’, path: path.resolve(__dirname, ‘dist’) }, module: { rules: [ { test: /.js$/, loader: ‘babel-loader’, exclude: /node_modules/ } ] }}.babelrc 文件:{ “presets”: [ ["@babel/preset-env", { “useBuiltIns”: “usage”, “modules”: false, “targets”: { “browsers”: “last 2 versions, not ie <= 9” } }] ], “plugins”: [ ["@babel/plugin-transform-runtime", { “helpers”: false }] ]}符录usuallyjs 项目是本人最近建设的开源项目,欢迎感兴趣的同行交流。usuallyjs: https://github.com/JofunLiang/usuallyjs ...

March 11, 2019 · 2 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

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

深入理解webpack打包机制(四)

有了webpack的模版 并且有了各个模块之间的依赖关系,接下来我们就可以实现打包。接下来就开始实现Compiler.js中的最终打包(即emit)方法:写emit()方法之前,首先要安装一下ejs模块,我们需要用ejs模版引擎来解析刚才手写的webpck模版。进入到my-pick目录, 运行命令:npm i ejs -DCompiler.js:let path = require(‘path’);let fs = require(‘fs’);let babylon = require(‘babylon’);let traverse = require(’@babel/traverse’).default;let t = require(’@babel/types’);let generator = require(’@babel/generator’).default;let ejs = require(’ejs’);class Compiler{ constructor(config){ this.config = config; this.entry = config.entry; this.entryId = ‘’; this.modules = {}; this.rootPath = process.cwd(); } run(){ this.buildModule(path.resolve(this.rootPath,this.entry),true); this.emit(); } buildModule(modulePath, isEntry){ let source = this.getSource(modulePath); let moduleName = ‘./’+path.relative(this.rootPath,modulePath); if(isEntry){ this.entryId = moduleName }; let { sourceCode, dependencies } = this.parse(source, path.dirname(moduleName)); this.modules[moduleName] = sourceCode; dependencies.forEach((depend)=>{ this.buildModule(path.join(this.rootPath,depend),false); }); } parse(source, parentPath){ let dependencies = []; let ast = babylon.parse(source); traverse(ast, { CallExpression(p){ if(p.node.callee.name === ‘require’){ p.node.callee.name = ‘webpack_require’; let moduleName = p.node.arguments[0].value; moduleName = moduleName + (path.extname(moduleName)?’’:’.js’); moduleName = ‘./’+path.join(parentPath,moduleName); dependencies.push(moduleName); p.node.arguments = [t.stringLiteral(moduleName)]; } } }); let sourceCode = generator(ast).code; return { sourceCode, dependencies }; } getSource(sourcePath){ return fs.readFileSync(sourcePath,‘utf8’); } emit(){ let main = path.join(this.config.output.path,this.config.output.filename); let templateStr = this.getSource(path.resolve(__dirname, ’template.ejs’)); let code = ejs.render(templateStr,{ entryId:this.entryId,modules:this.modules }); this.assets = {}; this.assets[main] = code; fs.writeFileSync(main,this.assets[main]); }}module.exports = Compiler;emit()方法就是最终我们实现webpack的打包方法。最后的bundle.js就是由该方法生成。首先,通过path.join()方法和config中的output 获取到最终的打包文件的路径。第二行又获取到之前写好的模版文件:template.ejs。最终通过ejs模块解析 并且传入主入口entryId和依赖关系对象modules生成最终的打包文件。生成了最终的打包文件后就很简单了,首先把最终的文件放到this.assets对象中,最后又通过fs.readFileSync()写入bundle.js文件到输出路径。最后,回到webpack的目录:运行自己的pick命令:npx my-pack. 即可看到dist目录中多了一个bundle.js的文件 bundle.js: (function(modules) { var installedModules = {}; function webpack_require(moduleId) { if(installedModules[moduleId]) { return installedModules[moduleId].exports; } var module = installedModules[moduleId] = { i: moduleId, l: false, exports: {} }; modules[moduleId].call(module.exports, module, module.exports, webpack_require); module.l = true; return module.exports; } return webpack_require(webpack_require.s = “./src/index.js”); }) /* 自执行函数 传入参数 */ ({ “./src/index.js”: (function(module, exports, webpack_require) { eval(console.log('index.js');__webpack_require__("./src/a.js");); }), “./src/a.js”: (function(module, exports, webpack_require) { eval(let b = __webpack_require__("./src/b.js");console.log('a.js');console.log(b);); }), “./src/b.js”: (function(module, exports, webpack_require) { eval(module.exports = 'b.js';); }), });可以看到 ,所以的依赖关系都被传递到了webpack的自执行函数的参数中。通过右键run,或者浏览器中打开。发现打印了我们写的代码:index.jsa.jsb.js到此为止,我们已经手动实现了webpack的打包功能。当然这只是webpack中的冰山一角,我们只是简单实现了解析模块的依赖关系,打包了js文件。像webpack的钩子,loader,plugins,css文件..等等都没有进行处理。可能会在以后的文章中会较为深入的解析webpack的loader和plugins是如何实现的。谢谢观看~喜欢点个???? ...

March 9, 2019 · 2 min · jiezi

详解webpack code splitting

webpack代码分割什么是代码分割在最开始使用Webpack的时候, 都是将所有的js文件全部打包到一个build.js文件中(文件名取决与在webpack.config.js文件中output.filename), 但是在大型项目中, build.js可能过大, 导致页面加载时间过长. 这个时候就需要code splitting, code splitting就是将文件分割成块(chunk), 我们可以定义一些分割点(split point), 根据这些分割点对文件进行分块, 并实现按需加载。代码分割,也就是Code Splitting一般需要做这些事情:为 Vendor 单独打包(Vendor 指第三方的库或者公共的基础组件,因为 Vendor 的变化比较少,单独打包利于缓存)为 Manifest (Webpack 的 Runtime 代码)单独打包为不同入口的业务代码打包,也就是代码分割异步加载(同理,也是为了缓存和加载速度)为异步公共加载的代码打一个的包来自蚂蚁金服数据体验技术Webpack 4 配置最佳实践,这里虽然用的webpack3,也同样试用。下面就用Code Splitting来实现上面几点。code splittingwebpack的代码分割Code Splitting,主要有两种方式:第一种:webpack1通过require.ensure定义分割点,将代码进行分割打包,异步加载。第二种:在动态代码拆分方面,webpack支持符合ECMAScript提议的import()语法进行代码分割和异步加载。require.ensure代码分割webpack 在编译时,会静态地解析代码中的 require.ensure(),同时将模块添加到一个分开的 chunk 当中,这个新的 chunk 会被 webpack 通过 jsonp 来异步加载,其他包则会同步加载。语法:require.ensure(dependencies: String[], callback: function(require), chunkName: String)参数:第一个参数是dependencies依赖列表,webpack会加载模块,但不会执行。第二个参数是一个回调,当所有的依赖都加载完成后,webpack会执行这个回调函数,在其中可以使用require导入模块,导入的模块会被代码分割到一个分开的chunk中。第三个参数指定第二个参数中代码分割的chunkname。将下面代码拷贝到webpack.config.RequireEnsure.js:var webpack = require(‘webpack’);var path = require(‘path’);module.exports = { entry: { ‘pageA’: ‘./src/pageA’, ‘vendor’: [’lodash’] // 指定单独打包的第三方库(和CommonsChunkPlugin结合使用),可以用数组指定多个 }, output: { path: path.resolve(__dirname, ‘./dist’), filename: ‘[name].bundle.js’, chunkFilename: ‘[name].chunk.js’, // code splitting的chunk是异步(动态)加载,需要指定chunkFilename(具体可以了解和filename的区别) }, plugins: [ // 为第三方库和和manifest(webpack runtime)单独打包 new webpack.optimize.CommonsChunkPlugin({ name: [‘vendor’, ‘manifest’], minChunks: Infinity }), ]}src/pageA.js、src/subPageA.js、src/subPageB.js、src/module.js代码如下:// src/pageA.jsimport * as _ from ’lodash’;import subPageA from ‘./subPageA’;import subPageB from ‘./subPageB’;console.log(’this is pageA’);export default ‘pageA’;// src/subPageA.jsimport module from ‘./module’;console.log(’this is subPageA’);export default ‘subPageA’;// src/subPageB.jsimport module from ‘./module’;console.log(’this is subPageB’);export default ‘subPageB’;// src/module.jsconst s = ’this is module’export default s;其中subPageA和subPageB模块使用共同模块module.js,命令行运行webpack –config webpack.config.RequireEnsure.js,打包生成:pageA.bundle.jsvendor.bundle.js // 为 Vendor 单独打包manifest.bundle.js // 为Manifest单独打包满足我们文档一刚开始说的代码分割的两点要求,但是我们想要subPageA和subPageB单独打包:修改src/pageA.js,把import导入方式改成require.ensure的方式就可以代码分割:// import subPageA from ‘./subPageA’;// import subPageB from ‘./subPageB’;require.ensure([], function() { // 分割./subPageA模块 var subPageA = require(’./subPageA’);}, ‘subPageA’);require.ensure([], function () { var subPageB = require(’./subPageB’);}, ‘subPageB’);再次打包,生成:pageA.bundle.jssubPageA.chunk.js // 代码分割subPageB.chunk.jsvendor.bundle.js // 为 Vendor 单独打包manifest.bundle.js // 为Manifest单独打包会发现用了require.ensure的模块被代码分割了,达到了我们想要的目的,但是由于subPageA和subPageB有公共模块module.js,打开subPageA.chunk.js和subPageB.chunk.js发现都有公共模块module.js,这时候就需要在require.ensure代码前面加上require.include(’./module’)src/pageA.js:require.include(’./module’); // 加在require.ensure代码前再次打包,公共模块modle.js被打包在了pageA.bundle.js,解决了为异步加载的代码打一个公共的包问题。最后测试一下webpack打包后的动态加载/异步加载:在index.html里面引入打包文件:<html> <body> <script src="./dist/manifest.bundle.js"></script> <script src="./dist/vendor.bundle.js"></script> <script src="./dist/pageA.bundle.js"></script> </body></html>webpack.config.RequireEnsure.js加上动态加载路径的配置后再次打包:output: { … publicPath: ‘./dist/’ // 动态加载的路径}浏览器中打开index.html文件,会发现subPageA.chunk.js和subPageB.chunk.js没有引入也被导入了进来。其实这两个文件是webpack runtime打包文件根据代码分割的文件自动异步加载的。<img src=“https://user-gold-cdn.xitu.io…;h=115&f=png&s=12335”>dynamic import 代码分割requre.ensure好像已被webpack4废弃。es6提供了一种更好的代码分割方案也就是dynamic import(动态加载)的方式,webpack打包时会根据import()自动代码分割;虽然在提案中,可以安装babel插件兼容,推荐用这种方式:语法:import(/* webpackChunkName: chunkName / chunk).then( res => { // handle something}).catch(err => { // handle err});其中/ chunkName /为指定代码分割包名,chunk指定需要代码分割的文件入口。注意不要把 import关键字和import()方法弄混了,该方法是为了进行动态加载。import()调用内部使用promises。如果您使用import()较旧的浏览器,请记住Promise使用诸如es6-promise或promise-polyfill之类的polyfill进行填充。下面修改pageA.js:import * as _ from ’lodash’;import(/ webpackChunkName: ‘subPageA’ /’./subPageA’).then(function(res){ console.log(‘import()’, res)})import(/ webpackChunkName: ‘subPageB’ */’./subPageB’).then(function(res){ console.log(‘import()’, res)})console.log(’this is pageA’);export default ‘pageA’;将下面代码拷贝到webpack.config.import.js:var webpack = require(‘webpack’);var path = require(‘path’);module.exports = { entry: { ‘pageA’: ‘./src/pageA’, ‘vendor’: [’lodash’] // 指定单独打包的第三方库(和CommonsChunkPlugin结合使用),可以用数组指定多个 }, output: { path: path.resolve(__dirname, ‘./dist’), filename: ‘[name].bundle.js’, chunkFilename: ‘[name].chunk.js’, // code splitting的chunk是异步(动态)加载,需要指定chunkFilename(具体可以了解和filename的区别) publicPath: ‘./dist/’ // 动态加载的路径 }, plugins: [ // 为第三方库和和manifest(webpack runtime)单独打包 new webpack.optimize.CommonsChunkPlugin({ name: [‘vendor’, ‘manifest’], minChunks: Infinity }), ]}命令行webpack –config webpack.config.import.js,打包发现和require.ensure一样的结果:pageA.bundle.jssubPageA.chunk.js // 代码分割subPageB.chunk.jsvendor.bundle.js // 为 Vendor 单独打包manifest.bundle.js // 为Manifest单独打包为了分离出subPageA.chunk.js和subPageB.chunk.js的公共模块module.js,可以用CommonsChunkPlugin的async方式给异步加载的打包提取公共模块:在webpack.config.import.js加上下面配置:(其实这种方式可以替代require.ensure中的require.include方式提取公共代码,更自动和简单)plugins: [ // 为异步公共加载的代码打一个的包 new webpack.optimize.CommonsChunkPlugin({ async: ‘async-common’, // 异步公共的代码 children: true, // 要加上children,会从入口的子依赖开始找 minChunks: 2 // 出现2次或以上相同代码就打包 }), …]重新打包,打包文件如下:pageA.bundle.jssubPageA.chunk.js // 代码分割subPageB.chunk.jsasync-common-pageA.chunk.js // 为异步公共加载的代码打的包vendor.bundle.js // 为 Vendor 单独打包manifest.bundle.js // 为Manifest单独打包会发现多了一个异步加载包subPageA.chunk.js和subPageB.chunk.js的公共模块async-common-pageA.chunk.js包,配置成功!同样,还是进行测试,index.html引入同步加载的包:<html> <body> <script src="./dist/manifest.bundle.js"></script> <script src="./dist/vendor.bundle.js"></script> <script src="./dist/pageA.bundle.js"></script> </body></html>浏览器中打开index.html文件,会发现subPageA.chunk.js、subPageB.chunk.js和async-common-pageA.chunk.js被自动异步加载了。<img src=“https://user-gold-cdn.xitu.io…;h=136&f=png&s=15038”>关于异步加载和按需加载关于文档中多次提到webpack代码分割的包,在浏览器中webpack runtime的包会自动异步加载代码分割的包,那么在react和vue应用中,如果这些代码分割包在页面初始化也会自动异步加载,那不是分包的作用不大?原因其实是我们上面例子执行了import()或require.ensure,而在应用中,写法是当请求路由的时候才执行import()或require.ensure,然后再异步加载,webpack遇到import()或require.ensure的配置的时候只会进行代码切割,这种思路就是按需加载的基础。部分参考链接:代码分割 - 使用 require.ensure文档中出现的源代码在我的github,感兴趣的欢迎star。 ...

March 9, 2019 · 2 min · jiezi

webpack学习笔记

简介webpack可以做的事代码转换文件优化代码分割模块合并自动刷新代码校验自动发布面试常见考点webpack常见配置webpack高级配置ast抽象语法树webpack中的Tapable掌握webpack流程,手写webpack手写webpack中常见的loader手写webpack中常见的pluginwebpack基础配置起步创建src—>index.jsnpx webpack基础配置//webpack是node写出来的,所以需要node的写法let path = require(‘path’) //核心模块module.exports = { mode: ‘development’, //默认两种:production development entry: ‘./src/index.js’, //入口 output: { //出口 filename: ‘bundle.js’, //打包后的文件名 path: path.resolve(__dirname, ‘dist’), //resolve把相对路径解析成绝对路径,__dirname意思是在当前目录建立一个,路径必须是一个绝对路径 }}script脚本手动配置:npx webpack –config webpack.config.js脚本配置:“scripts”: { “build”: “webpack” }//npm run build 传参npx webpack – –config webpack.config.jsHtml插件npx webpack-dev-server开发服务,内部通过express实现这种服务并不真实打包文件,只是在内存中生成htmlWebpackPlugin将打包后的js文件插入html文件,并放到build目录下let htmlWebpackPlugin = require(‘html-webpack-plugin’)module.exports = { devServer: { //开发服务器的配置 port: 3000, //设置端口号 progress: true, //进度条 contentBase: ‘/.build’, //指定静态目录 compress: true }, output: { filename: ‘bundle.[hash:8].js’, //文件名 path: path.resolve(__dirname, ‘dist’), }, plugins: [ //数组 放着所有的webpack插件 new htmlWebpackPlugin({ template: ‘./src/index.html’, //模板 filename: ‘index.html’, //打包后的文件名 minify: { //打包后的html也压缩 removeAttributeQuotes: true, //删除属性的双引号 collapseWhitespace: true, //折叠空行 }, hash: true //html文件加上哈希戳 }) ]}css配置loader:Webpack本身只能处理 JavaScript 模块,如果要处理其他类型的文件,就需要使用 loader 进行转换。Loader 可以理解为是模块和资源的转换器,它本身是一个函数,接受源文件作为参数,返回转换的结果css配置css-loader 解析@import这种语法style-loader把css插入到head的标签中loader的特点:希望单一loader的用法:字符串只用一个loader,多个loader需要[]loader的顺序:默认是从右向左执行 从下往上优先级{ loader: ‘style-loader’, options: { insertAt: ’top’ //确保优先级 }}处理less、sass、stylusyarn add less less-loaderyarn add node-sass sass-loaderyarn add stylus stylus-loadermodule.exports = { module: { //模块 rules: [ //规则 //loader的特点:希望单一 //{ test: /.css$/, use: [‘style-loader’, ‘css-loader’] } //第一种:写法 //loader还可以写成对象方式 { //处理less文件 test: /.css$/, use: [{ loader: ‘style-loader’, options: { insertAt: ’top’ //确保优先级 } }, ‘css-loader’, //@import 解析路径 ’less-loader’ //把less —->css ] } ] }}抽离CSS的插件默认打包后只能插入<style>标签内,我们希望抽离成<link>形式通过 mini-css-extract-plugin这个插件yarn add mini-css-extract-plugin -D插件都是类,插件的使用顺序没有先后let MiniCssExtractPlugin = require(‘mini-css-extract-plugin’)//配置pluginplugins: [ new MiniCssExtractPlugin({ filename: ‘main.css’, }) ],//配置module module: { rules: [{ test: /.css$/, use: [MiniCssExtractPlugin.loader, ‘css-loader’] }, { test: /.less$/, use: [MiniCssExtractPlugin.loader, ‘css-loader’, ’less-loader’] } ] } 自动添加前缀autoprefixer前提要用postcss-loaderyarn add postcss-loader autoprefixer//配置module//先处理post-css再处理cssmodule: { rules: [{ test: /.css$/, use: [MiniCssExtractPlugin.loader, ‘css-loader’,‘postcss-loader’] //加上post-css }, { test: /.less$/, use: [MiniCssExtractPlugin.loader, ‘css-loader’, ‘postcss-loader’, ’less-loader’] } ] }//创建postcss.config.js文件并配置module.exports = { plugins: [require(‘autoprefixer’)]}压缩css(同时保证js的压缩)通过optimize-css-assets-webpack-pluginyarn add optmize - css - assets - webpack - plugin - Duglifyjs-webpack-pluginlet path = require(‘path’)let htmlWebpackPlugin = require(‘html-webpack-plugin’)let MiniCssExtractPlugin = require(‘mini-css-extract-plugin’)let OptimizeCss = require(‘optimize-css-assets-webpack-plugin’)let UglifyJsPlugin = require(‘uglifyjs-webpakc-plugin’)module.exports = { Optimization: { //***优化项,用了这个插件之后就必须用一下Uglifyjs压缩js minimizer: [ new UglifyJsPlugin({ cache: true, //是否用缓存 parallel: true, //是否并行打包 sourceMap: true }), new OptimizeCss() ] }, devServer: { port: 3000, progress: true, contentBase: ‘/.build’ }, mode: ‘development’, entry: ‘./src/index.js’, output: { filename: ‘bundle.[hash:8].js’, path: path.resolve(__dirname, ‘dist’), }, plugins: [ new htmlWebpackPlugin({ template: ‘./src/index.html’, filename: ‘index.html’, minify: { removeAttributeQuotes: true, collapseWhitespace: true, }, hash: true }), new MiniCssExtractPlugin({ filename: ‘main.css’ }) ], module: { rules: [ //{ test: /.css$/, use: [‘style-loader’, ‘css-loader’] } { test: /.css$/, use: [ MiniCssExtractPlugin.loader, //**创建link标签,引用 ‘css-loader’, ’less-loader’, ‘postcss-loader’ ] } ] }}JS配置转化es6语法babelyarn add babel-loader @babel/core @babel/preset-env -D配置modulemodule: { rules: [{ test: /.js$/, use: { loader: ‘babel-loader’, options: { //用babel-loader es6—->es5 presets: [ ‘@babel/preset-env’ ] } } } ] } 配置提案里支持的语法class A{ a = 1;}yarn add @babel/plugin-proposal-class-properties -D{ test: /.js$/, use: { loader: ‘babel-loader’, options: { presets: [ ‘@babel/preset-env’ ], plugins: [ ‘@babel/plugin-proposal-class-properties’ ] } }}支持装饰器语法function log(target) { console.log(target, ‘23’)}log(A)yarn add @babel/plugin-proposal-decorators -D配置modulemodule: { rules: [{ test: /.js$/, use: { loader: ‘babel-loader’, options: { //用babel-loader es6—->es5 presets: [ ‘@babel/preset-env’ ], plugins: [ [’@babel/plugin-proposal-class-properties’], [’@babel/plugin-proposal-decorators’,{“legacy”:true}], [’@babel/plugin-transform-runtime’] ] } } } ] }处理JS语法及校验 function gen(params){ yield 1; } console.log(gen().next()); //内置API // Uncaught ReferenceError: regeneratorRuntime is not definedbabel-runtimeyarn add @babel/plugin-transform-runtime -Dyarn add @babel/runtimemodule: { rules: [{ test: /.js$/, use: { loader: ‘babel-loader’, options: { //用babel-loader es6—->es5 presets: [ ‘@babel/preset-env’ ], plugins: [ [’@babel/plugin-proposal-class-properties’], [’@babel/plugin-proposal-decorators’,{“legacy”:true}], [’@babel/plugin-transform-runtime’] ] } }, include:path.resolve(__dirname,‘src’), //包括查找范围 exclude:/node_module/ //排除查找范围 } ] }@babel/polyfill’aaa’.include(‘a’) //ES7语法//实例上的方法默认都不会解析 yarn add @babel/polyfill//a.jsrequire(’@babel/polyfill’)‘aaa’.include(‘a’)eslintyarn add eslint eslint-loadermodule: { rules: [ //loader默认从右向左执行 从下到上,写的太多容易乱,写到一起删除不方便 { test:.js$/, use:{ loader:’eslint-loader’, options:{ enforce:‘pre’ //previous -> normal-> post 顺序执行 } } } /…/ ]} 选择好配置,下载.eslintrc.json到configuration全局变量引入问题第三方模块引用yarn add jqueryimport $ form ‘jqurey’console.log($) console.log(window.$) //undefined,并不会挂载到window上如何将变量暴露给window?expose-loader (内联loader)yarn add expose-loaderimport $ from ’expose-loader?$!jquery’ //将jquery作为$暴露给全局也可以在webpack.config.js中配module: { rules: [{ test: require.resolve(‘jquery’), use: ’expose-loader?$!jquery’ } ] } 在每个模块中注入$对象//webpack.config.jslet Webpack = require(‘webpack’) new Webpack.ProvidePlugin({ //再每个模块中都注入$符 jqurey: ‘$’})//index.jsconsole.log($) //只是在每个模块中都注入一个$//此时window.$ //undefinedcdn外部路径引入<script src=“https://cdn.bootcss.com/jquery/3.3.1/core.js"></script>//如果此时js也引入jquery会导致重复import $ form jqueryexternals: { jquery: ‘jQuery’ //告诉webpack从外部引入,并不需要打包,忽略 },总结expose-loader:暴露到window上providePlugin:给每个提供一个引入不打包loader类型pre 前面执行的loadernormal 普通的loaderliader 内联loaderpost 后置loader图片处理图片引入方式在js中创建图片来引入let image = new Image()image.src = ‘./logo.png’document.body.appendChild(image) //打包完,其实就是一个普通的字符串,并没有真正的引入图片file-loaderyarn add file-loader默认在内部生成图片,到build目录下,并且把生成的路径的名字返回回来//webpack.config.jsmodule: { rules: [{ test: /.(png|jpg|gif)$/, use: ‘file-loader’ } ]}//index.jsimport logo from ‘./logo.png’ //把图片引入,生成一个哈希戳的logo,返回的结果是一个新的图片console.log(logo)let image = new Image();image.src = logo;document.body.appendChild(image)在css中引入background(‘url’)<img src=”" alt="">打包文件分类打包多页应用配置source-map ...

March 9, 2019 · 3 min · jiezi

从24M到1M: 一个react+antd后台系统构建打包历程

虽然在工作中用react+antd写页面写了一年,但从来没自己去认认真真配置一个webpack,去分析去优化自己打出的包。在工程化成熟或者大点的公司,都有自己的打包工具,所以自己工作中很少去琢磨这些。为了试一下写出的组件库(antd-doddle)性能,就尝试自己去写一个webpack构建,真的是吓了自己一跳,流水账(tu)开始。从npm run dev开始打包工具:webpack4 + babel6 开始前,大概说一下项目内容。项目基于react+react-router+antd+antd-doddle,自己日常在这个项目做一些技术验证与demo,就4个页面。组成如下: <Content style={{ margin: ‘24px 16px’, padding: 24, background: ‘#fff’, minHeight: 280 }}> <Switch> <Route exact path={menus.home.path} component={Home} /> <Route exact path={menus.renderProps.path} component={ExampleTable} /> <Route exact path={menus.hoc.path} component={Hoc} /> <Route exact path={menus.learnTest.path} component={LearnTest} /> </Switch> </Content>页面js与公共(node_modules引用)js使用splitChunks分开打包。npm run dev,结果如下图,async.bundle.js大小24M,有点惊人的大。打包工具:webpack4 + babel7 开始正题,npm run builddevelopment与production由于webpack在(mode)模式development与production还是有很大区别,当我直接将mode从development变成production,再运行npm start。打包大小还是有明显的变化,同时也报了三个警告,包体积大于244kb打包大小从24M缩减到17M,由于run start是启用的webpack-dev-server来构建的,所以对应的production模式默认启用UglifyJsPlugin是无效的,但是可以手动去加入这段配置,然后打包大小能看到直线下降到3.25M,见下图 optimization: { minimizer: [ new UglifyJsPlugin() ], splitChunks: { name: ‘async’, minSize: 30000, cacheGroups: { vendors: { test: /[\/]node_modules[\/]/, chunks: ‘all’ }, } } }production模式构建以上其实都是在用webpack-dev-server构建,当用webpack去构建时,然后npm run build,结果是这样的: 压缩后大小已经减小到2.6M,还是有实质的进步,然后通过BundleAnalyzerPlugin这个插件看包的构成,会发现antd,antd-doddle,moment占了绝大多数的空间。 恰好我知道moment虽然不支持按需打包,由于支持了多语言,所以省去无用的语言选项,就可以有明显的优化,所以使用ContextReplacementPlugin这个插件,并加上只匹配中文的筛选,如下所示: new webpack.ContextReplacementPlugin( /moment[/\]locale$/, /zh-cn/, )打包体积由2.6减少到2.3M。当一个一个浏览完包的构成时,大小组成是这样的:atnd(609kb);antd-icon(477kb);antd-doddle(391kb)dragt.js(160kb)immutable(55*2kb)moment(53 *2kb)react-dom(100kb)others(400kb)我留下了一些疑问:发现有两个moment.js,两个immutable.js,一个引用路径是正常的,一个是包里的相对路径。然后还有一个draft.js(引用来源是antd),一个富文本编辑库,但我并没有这相关的组件应用,所以被打进来也是不讲道理的。然后看起来antd是按需引入(es module),但是我项目就4个页面,组件最多也就用到他所拥有的1/3,但是上面显示的285个,我的antd-doddle就更吓人了,居然有138个,我库中总共也就30来个导出,所以也是诡异。为了减少大小,我还使用了babel-plugin-import插件,但没有实质性的改变,然后看到包的路径是es,说明现有的构建本来就已经做了tree-shaking,所以这确实是无用功。但公司项目和网上大多数文章提到的,都是打包都能优化到1M左右,所以应该还有优化的空间。后面我干了什么呢?升级才是硬道理打包工具:webpack4 + babel7 babel6升到babel7,修改一下配置。然后再运行npm run build, 结果如下:当一个一个浏览完包的构成时,大小组成是这样的:atnd(367kb);antd-icon(475kb);react-dom(102kb)antd-doddle(66kb)immutable(55kb)moment(53kb)others(218kb)不能说是奇迹,但确实解决了一些上面留下的疑问:moment,immutable重复打包;draft无端引入;antd与antd-doddle包数激增。大小也减少了至少1M,所以升级才是自然界应有的规律,更不要说前端。 babel6到babel7,到底发生了什么看到这种优化的效果真的让我惊喜与惊讶,但究竟是什么带来了这种变化更让我更好奇。还没抓到核心的区别,有清楚的,请指点一下,怎么避免js公共函数的重复注入,怎么来优化编译来完美对接后面webpack打包的?想往外去看看大神们都什么意见,可特殊时期,我的梯子被折断了(衰)。文章提到的项目地址:webpack打包,master分支是对应babel6,roadhg分支是对应babel7 ...

March 7, 2019 · 1 min · jiezi

深入理解webpack打包机制(三)

有了依赖关系和解析后的源码后,就可以进行打包操作了。但是,还缺少一样东西,模版。模版是webpack中很重要的一环,它根据模块间的依赖关系生成不同参数,注意 是参数。这样说还是蛮抽象的,可以看一下真正的webpack打包后的bundle.js文件长啥样儿:bundle.js:// (function(modules) { // webpackBootstrap// // The module cache// var installedModules = {};//// // The require function// function webpack_require(moduleId) {//// // Check if module is in cache// if(installedModules[moduleId]) {// return installedModules[moduleId].exports;// }// // Create a new module (and put it into the cache)// var module = installedModules[moduleId] = {// i: moduleId,// l: false,// exports: {}// };//// // Execute the module function// modules[moduleId].call(module.exports, module, module.exports, webpack_require);//// // Flag the module as loaded// module.l = true;//// // Return the exports of the module// return module.exports;// }////// // expose the modules object (webpack_modules)// webpack_require.m = modules;//// // expose the module cache// webpack_require.c = installedModules;//// // define getter function for harmony exports// webpack_require.d = function(exports, name, getter) {// if(!webpack_require.o(exports, name)) {// Object.defineProperty(exports, name, { enumerable: true, get: getter });// }// };//// // define __esModule on exports// webpack_require.r = function(exports) {// if(typeof Symbol !== ‘undefined’ && Symbol.toStringTag) {// Object.defineProperty(exports, Symbol.toStringTag, { value: ‘Module’ });// }// Object.defineProperty(exports, ‘esModule’, { value: true });// };//// // create a fake namespace object// // mode & 1: value is a module id, require it// // mode & 2: merge all properties of value into the ns// // mode & 4: return value when already ns object// // mode & 8|1: behave like require// webpack_require.t = function(value, mode) {// if(mode & 1) value = webpack_require(value);// if(mode & 8) return value;// if((mode & 4) && typeof value === ‘object’ && value && value.__esModule) return value;// var ns = Object.create(null);// webpack_require.r(ns);// Object.defineProperty(ns, ‘default’, { enumerable: true, value: value });// if(mode & 2 && typeof value != ‘string’) for(var key in value) webpack_require.d(ns, key, function(key) { return value[key]; }.bind(null, key));// return ns;// };//// // getDefaultExport function for compatibility with non-harmony modules// webpack_require.n = function(module) {// var getter = module && module.__esModule ?// function getDefault() { return module[‘default’]; } :// function getModuleExports() { return module; };// webpack_require.d(getter, ‘a’, getter);// return getter;// };//// // Object.prototype.hasOwnProperty.call// webpack_require.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };//// // webpack_public_path// webpack_require.p = “”;////// // Load entry module and return exports// return webpack_require(webpack_require.s = “./src/index.js”);// })//// ({// “./src/a.js”:/!!\ ! ./src/a.js ! *//! no static exports found /// (function(module, exports, webpack_require) {eval("\nlet b = webpack_require(/! ./b / "./src/b.js");\n\nconsole.log(‘a.js’);\nconsole.log(b);\n\n//# sourceURL=webpack:///./src/a.js?");// }),// “./src/b.js”:/!!\ ! ./src/b.js ! **//! no static exports found /// (function(module, exports) {eval(" module.exports = ‘b.js’\n\n//# sourceURL=webpack:///./src/b.js?");// }),// “./src/index.js”:/!!\ ! ./src/index.js ! *//! no static exports found /// (function(module, exports, webpack_require) {eval(“console.log(‘index.js’);\n\n__webpack_require(/! ./a / "./src/a.js");\n\n//# sourceURL=webpack:///./src/index.js?”);// })/****/ });这一坨是啥?很乱对吧,把注释和一些无关紧要的代码去掉呢?长这样: (function(modules) { var installedModules = {}; function webpack_require(moduleId) { if(installedModules[moduleId]) { return installedModules[moduleId].exports; } var module = installedModules[moduleId] = { i: moduleId, l: false, exports: {} }; modules[moduleId].call(module.exports, module, module.exports, webpack_require); module.l = true; return module.exports; } return webpack_require(webpack_require.s = “./src/index.js”); }) ({ “./src/a.js”: (function(module, exports, webpack_require) {eval("\nlet b = webpack_require(/! ./b / "./src/b.js");\n\nconsole.log(‘a.js’);\nconsole.log(b);\n\n//# sourceURL=webpack:///./src/a.js?"); }), “./src/b.js”: (function(module, exports) {eval(" module.exports = ‘b.js’\n\n//# sourceURL=webpack:///./src/b.js?"); }), “./src/index.js”: (function(module, exports, webpack_require) {eval(“console.log(‘index.js’);\n\n__webpack_require__(/! ./a */ "./src/a.js");\n\n//# sourceURL=webpack:///./src/index.js?”); }) });注意最后一段,是不是和我们生成的this.modules对象的依赖关系很像?其实整个打包后的内容就是一个webpack的自执行函数,下面括号中那一坨就是该函数的参数。不过这个参数是key,value的形式,也就是依赖路径和解析后的源码的形式。未完待续… ...

March 7, 2019 · 3 min · jiezi

Webpack4 学习笔记 - 06:使用 Babel 处理 ES6 语法

修改 index.js 内容,写一些 ES6 的语法:const arr = [ new Promise(() => {}), new Promise(() => {})];arr.map(item => { console.log(item);})ES6 很强大,但目前并不是所有的浏览器都支持,所以需要用到 Babel,让旧的浏览器或环境中将 ES6 代码转换为向后兼容版本的 JavaScript 代码。来试一下吧,先安装需要用的 Babel 包:npm install babel-loader @babel/core -D配置 webpack.config.js,增加一条 rulues : module: { rules: [{ test: /.js$/, exclude: /node_modules/, // 排除该目录下的所有代码 loader: “babel-loader” }] }babel-loader 告诉了 webpack 怎么处理 ES6 代码,但它并不会将ES6 代码翻译成向后兼容版本的代码,如果想要执行这一步,还需要安装一个模块 preset-env,它包含了所有 ES6 代码转换的规则:npm install @babel/preset-env -D安装完之后配置一下:rules: [{ test: /.js$/, exclude: /node_modules/, // 排除该目录下的所有代码 loader: ‘babel-loader’, options:{ ‘presets’: [’@babel/preset-env’] }}]这样,运行打包命令,就可以把 ES6 语法翻译成 ES5了,看一下打包的结果:没问题,语法已经翻译成了当前所有浏览器能识别的语法,但是做到了这一点还是不够,因为那些比较新的对象和函数,比如这里的 Promise 和 map,在低版本的浏览器里实际还是不存在的。所以这时不仅要进行语法的转换,还要想办法把这些新的特性,补充到低版本的浏览器里。怎么做呢? babel 提供了一个工具叫 polyfill,安装:npm install @babel/polyfill -D然后在 index.js 的最顶部,引入这个包:import ‘@babel/polyfill’保存代码,再次进行打包查看结果,可以发现打包后的 main.js 里面,有了很多代码来帮助实现比如 Promise 和 map 这些新特性。看一下 main.js 文件的大小:859KB,再看一下没有使用 polyfill 之前的 main.js 大小:只有4.36KB,使用 polyfill 之后文件变大了很多,这说明了 polyfill 使用了非常多的代码来填入新特性。但是,index.js 里只使用了 Promise 和 map,其它的新特性都没用,能不能把那些没用到的实现方法都剔除了呢? 可以,给 preset-env 增加一个 useBuiltIns 配置:rules: [{ test: /.js$/, exclude: /node_modules/, // 排除该目录下的所有代码 loader: ‘babel-loader’, options: { ‘presets’: [ [’@babel/preset-env’, { useBuiltIns: ‘usage’ }] ] }}]useBuiltIns: ‘usage’ 的意思就是说,当使用 polyfill 往低版本浏览器填入一些不存在的特性时,不是全部都填入,而是根据业务代码使用到的特性去选择填入,比如这里使用了 Promise 和 map,那就只填入这两个,其它的都不用。 再次打包查看结果:可以看到,main.js 的大小只有 138KB了。这里还可以配置一些其它的参数,比如 targets 参数:rules: [{ test: /.js$/, exclude: /node_modules/, // 排除该目录下的所有代码 loader: ‘babel-loader’, options: { ‘presets’: [ [’@babel/preset-env’, { useBuiltIns: ‘usage’, targets:{ edge: ‘17’, // edge高于17的版本 firefox: ‘60’, // firefox 高于60的版本 chrome: ‘67’ // chrome高于67的版本 } }] ] }}]targets 是指打包会运行在什么样的浏览器,这有三个浏览器,并注明了最低版本。在打包的过程中,babel 会去看这些浏览器对 ES6 代码的支持情况,是否有必要进行语法转换、填入一些新特性。 运行打包命令查看结果:发现还是输出的 Promise 和 map,并没有进行新特性的填入,说明这三个版本的浏览器对 ES6 的支持已经很好了,不需要在进行额外的处理,main.js 的大小是变成了最初的4.36KB。到此为止,webpack 对 ES6 的简单处理就完成了。 关于 babel 还有很多东西和配置项,更多的知识要到 https://babel.docschina.org 来学习。 ...

March 7, 2019 · 1 min · jiezi

全局SASS/SCSS变量在Vue项目中应用解决方案

场景说明// 这是一个存放变量的scss文件 “@/styles/_variables.scss”// color font …$cf-light: #B6B6B6;$cf-gray: #8C8C8C;$cf-med: #505050;$cf-dark: #333333;$cf-highlight: #1775F0;我要在其他文件内都用这个来保证样式统一。比如某个组件<template> <div class=“notice”>注意!</div></template><style lang=“scss” scoped>.notice { color: $cf-highlight;}</style>这样就报错了。要改成下面这样<template> <div class=“notice”>注意!</div></template><style lang=“scss” scoped>@import “@/styles/_variables.scss”;.notice { color: $cf-highlight;}</style>简单描述一下:做Vue项目的时候,有时候我们预先设置了一个主题样式文件(_variables.scss),存放大量的定义的SASS变量,需要在不同的组件中使用,默认是无法使用的,除非每个组件内都引入这个_variables.scss文件,十分麻烦,这里提供几种方案。解决办法我有几个解决方案,理论上都可行,大家不妨根据实际应用场景来实践一下。使用sass-resources-loader如果项目使用Vue-cli 2/3,或者Vue项目用的Webpack,用这个loader都是可以的。官方对于各种场景已经写的很清楚了,请看sass-resources-loader。具体不说明了。Vue-cli 3.x 下的最方便的方案这个我还没实践,不过应该是可行的。。。给小白们自己去试,好用的话记得留言回复下哦打开vue.config.js文件,进行如下配置:module.exports = { css: { loaderOptions: { sass: { data: @import "@/styles/_variables.scss"; } } }};具体细节,请阅读:Globally Load SASS into your Vue.js ApplicationsHow to Import a Sass File into Every Vue Component in an App这两篇原理相同,就是细节上有点不同,怕有的打不开就放两个给大家研究下。Nuxt这里还是接住一个插件style-resources-module,这个最近才出的,高级很多,在他之前,都是用nuxt-sass-resources-loader,如果你的项目还在用旧的,可以换成新的。nuxt-sass-resources-loader官方也说了不在更新维护,建议使用style-resources-module。怎么用呢?这里有Example,我也复制一份,醒目。打开nuxt.config.jsexport default { modules: [’@nuxtjs/style-resources’], styleResources: { scss: [ ‘./assets/styles/_variables.scss’, ‘./assets/styles/mixins.scss’ // use underscore “” & also file extension “.scss” ] }}自己注意文件路径结语现在不用每个组件都写导入变量文件了,是不是轻松多了,也不会因为文件名,路径调整,而胆战心惊的文件批量替换。我为什么写这个文章,因为虽然以前研究过,但是时代变化很快,一些更好的方案出现了,但是很多人依旧采用旧的,可能在新的项目上带来一些问题,所以就更新了。(小字,看不见):其次,我其实在使用easywebpack的egg+vue脚手架遇到了这个问题,搞了半天没搞好。。。去官方群里问没人鸟我,于是凄惨退群(底层技术渣的待遇)。参考:Load a global settings.scss file in every vue component? ...

March 7, 2019 · 1 min · jiezi

深入理解webpack打包机制(二)

可以使用npx my-pick命令后,我们就可以在my-pick中编写自己的webpack:webpack.config.js中导出的是一个对象,这个对象是webpack的配置参数。说白了,导出对象的入口 出口 module plugins…什么的 全是webpack参数中的其中一个。那webpack就可以理解为一个很大的函数,把参数传递进去 返回结果。1 在bin同级目录 创建lib目录,用来存放打包的核心的配置文件 -> mkdir lib -> cd /lib -> touch Compiler.js2 在my-pick.js中引入webpack.config.js的配置#! /usr/bin/env nodelet path = require(‘path’);let config = require(path.resolve(‘webpack.config.js’));let Compiler = require(’../lib/Compiler’);let compiler = new Compiler(config);compiler.run();在my-pick.js中, 首先通过path模块引入了webpack.config.js,其次 又引入了lib/Compiler.js,不难发现,Compiler是一个类,并且通过new,new出了Compiler的实例对象compiler。最后执行compiler的run()方法。3 接下来就要在lib/Compiler.js中新建Compiler类let path = require(‘path’);let fs = require(‘fs’);class Compiler{ constructor(config){ this.config = config; this.entry = config.entry; this.entryId = ‘’; this.modules = {}; this.rootPath = process.cwd(); } run(){ this.buildModule(path.reaolve(this.rootPath,this.entry),true); this.emit(); } buildModule(modulePath, isEntry){ } emit(){ }}module.exports = Compiler;constructor中传入了刚才获取到的webpack.config.js的配置对象config,拿到配置中的入口entry,entryId用来存放主入口,modules对象用来存放依赖key和源码value,rootPath是当前工作路径,类似于dirname。 原型上添加了run方法,run方法内部又执行了buildModule()和emit()方法。buildModule方法的作用是通过传入的路径,获取到文件源码,解析模块之间的依赖(不是它的主要作用)。emit方法作用是把最后解析好的源码和依赖关系发射出去。下面开始写buildModule()方法:let path = require(‘path’);let fs = require(‘fs’);class Compiler{ constructor(config){ this.config = config; this.entry = config.entry; this.entryId = ‘’; this.modules = {}; this.rootPath = process.cwd(); } run(){ this.buildModule(path.resolve(this.rootPath,this.entry),true); this.emit(); } buildModule(modulePath, isEntry){ let source = this.getSource(modulePath); let moduleName = ‘./’+path.relative(this.rootPath,modulePath); if(isEntry){ this.entryId = moduleName }; let { sourceCode, dependencies } = this.parse(source, path.dirname(moduleName)); } getSource(sourcePath){ return fs.readFileSync(sourcePath,‘utf8’); } emit(){ }}module.exports = Compiler;buildModule()传入了两个参数。第一个参数是文件的路径,在本次代码中是主入口路径,但是之后不一定是主入口的路径。第二个参数是判断该路径是否为主入口的表示,true表示是主入口,false表示不是主入口。很显然,本次调用的buildModule()函数是主入口,所以第二个参数传递的是true。buildModule函数第一行表示根据路径获取到源码,根据单独写的this.getSource()方法。第二行是把传入的绝对路径转换为相对路径。第三行是判断是否是主入口,是的话就把该路径保存到主入口entryId中。第四行中又多了一个parse()方法,parse()方法返回了源码sourceCode和依赖dependencies。它是核心方法,这个函数传入了源码和路径,注意这个路径是父级路径,path.dirname()。比如 原路径是‘./src/index.js’,父路径就是’./src’。为什么这样写,待会在parse()中会体现出来。未完待续… ...

March 6, 2019 · 1 min · jiezi

vue2.5.2+webpack3.6.0环境下es6语法解释配置

前置准备下载对应的loader第一步配置webpack配置路径:webpack.base.js->module->rules->属性解析:test 使用正则语法匹配js文件loader js文件加载器include 需要处理的路径exclude 不需要处理的路径第二步配置babel配置路径:根目录下建立.labelrc属性解析:“presets”:预设前置,里面包含的每个字段都要下载对应的npm包,比如 “env” -> npm install babel-preset-env “stage-2” -> npm install babel-preset-stage-2结尾根据上面步骤大概能解决问题

March 6, 2019 · 1 min · jiezi

深刻理解webpack

本文需要知道的两个npm命令 npx 和 npm linknpx:我们知道,npm5.2.0 引入了npx命令,当我们打包时 可以直接npx webpack,也可以实现打包npm link:npm link命令可以将一个任意位置的npm包链接到全局执行环境,从而在任意位置使用命令行都可以直接运行该npm包。也就是说 我们可以把我们自己模拟的webpack link到全局,进而也可以实现类似npx ..pack 的操作。工欲善其事,必先利其器,下面开始配置属于我们自己的npx命令:1 mkdir my-pick 创建目录my-pick2 npm init -y 初始化3 package.json中添加bin字段 注意用双引号:{ “name”: “my-pick”, “version”: “1.0.0”, “description”: “”, “main”: “index.js”, “bin”: { “my-pack”: “./bin/my-pick.js” }, “scripts”: { “test”: “echo "Error: no test specified" && exit 1” }, “keywords”: [], “author”: “”, “license”: “ISC”}4 在文件夹my-pick中 mkdir bin 创建bin目录5 cd /bin6 touch my-pick.js7 在my-pick.js中的第一行添加:#! /usr/bin/env node 指定运行环境为node#! /usr/bin/env nodeconsole.log(’this is my pick.’);8 cd ../ 到my-pick目录9 sudo npm link 回车 输入mac密码10 可以看到:/usr/local/bin/my-pack -> /usr/local/lib/node_modules/git-webpack/bin/my-pick.js/usr/local/lib/node_modules/git-webpack -> /Users/apple/Desktop/my-pack每人的电脑目录名称不同,出现类似这种就表示link到全局成功。配置打包目录11 另外新建一个新目录 mkdir my-webpack12 进入并初始化目录 npm init -y13 安装webpack npm i webpack webapck-cli -D14 新建src目录 mkdir src15 cd /src 16 为了实现模块间的互相引用 创建三个文件 touch index.js a.js b.js index.js:console.log(‘index.js’);require(’./a’);a.js:let b = require(’./b’);console.log(‘a.js’);console.log(b);b.js: module.exports = ‘b.js'17 新建webpack配置文件 touch webapck.config.js18 配置webpack.config.jswebpack.config.js:let path = require(‘path’);module.exports = { mode:‘development’, entry:’./src/index.js’, output:{ filename:‘bunle.js’, path:path.resolve(__dirname,‘dist’) }}19 命令行运行 npx webpackFunkyTiger:my-webpack apple$ npx webpackHash: a62b20a12c5ee84b0357Version: webpack 4.29.6Time: 88msBuilt at: 2019-03-06 11:51:36 Asset Size Chunks Chunk Namesbunle.js 4.43 KiB main [emitted] mainEntrypoint main = bunle.js[./src/a.js] 62 bytes {main} [built][./src/b.js] 25 bytes {main} [built][./src/index.js] 41 bytes {main} [built]这个简单项目即打包成功20 最后一步 把刚才npm link到全局的命令npx my-pick 再link到本地中使用21 运行命令: npm link my-pick 22 出现:/Users/apple/Desktop/git-webpack/my-webpack/node_modules/my-pick -> /usr/local/lib/node_modules/my-pick -> /Users/apple/Desktop/git-webpack/my-pick即表示link到本地成功 。23 运行命令 npx my-pick 出现:FunkyTiger:my-webpack apple$ npx my-pickmy-pick打印出了 刚写的日志 my-pick. 即可使用自己的命令npx my-pick 来实现自己的webpack。未完待续… ...

March 6, 2019 · 1 min · jiezi

Webpack 4 学习总结

一、安装配置【前提】安装node.js环境【官网下载】https://nodejs.org/zh-cn/安装教程不赘述创建项目文件夹例如创建如下文件夹webpack_demo1创建配置项npm init -y生成一个package.json文件 如下图全局安装webpack (不推荐,进行下一步操作)npm install webpack -g局部安装webpack(推荐)npm install webpack –save-dev另外,webpack 4要求安装包npm install webpack-cli –save-dev一起安装npm install webpack webpack-cli –save-dev以下表示安装成功创建入口文件./src/index.js任意编写index.js文件内容用于测试配置生产和开发模式打开package.json文件添加如下脚本"scripts": { “dev”: “webpack –mode development”, “build”: “webpack –mode production”}现在运行:npm run dev生成dist/main.js ,其中 main.js 没有压缩npm run dev 表示开发模式npm run build此时的main.js 被压缩 ,这便是生产模式接下来看看打包的js文件是否能够使用创建index.html 引入打包好的main.js<!DOCTYPE html><html lang=“en”><head> <meta charset=“UTF-8”> <meta name=“viewport” content=“width=device-width, initial-scale=1.0”> <meta http-equiv=“X-UA-Compatible” content=“ie=edge”> <title>Document</title> <script src="./main.js" charset=“utf-8”></script></head><body></body></html>打开浏览器调试输出结果表示打包成功,大功告成 !!!二、使用配置文件进行打包上一讲中我们打包没有用到webpack.config.js配置文件,webpack4把自己定位为一个零配置的工具。这一讲学习配置文件使用,更好地学习webpack。根目录下新建一个webpack.config.js文件 (记载配置信息)配置文件 const path = require(‘path’); module.exports = { entry:’./public/index.js’, output:{ path:path.resolve(__dirname,‘build’), filename:‘bundle.js’ } }字段意义entry入口,所需打包的文件的路径output出口path文件打包后存放的路径path.solve()将路径或者路径片段的序列处理成绝对路径_dirname表示当前文件所在目录的绝对路径filename打包后文件的名称按照配置项新建一个入口文件public/index.js运行npm run dev三、配置入口和出口的进阶使用一、单出口形式webpack.config.jsconst path = require(‘path’);module.exports = { //单出口形式 entry:[’./public/index.js’,’./public/index2.js’],//有多个文件 output:{ path:path.resolve(__dirname,‘build’), filename:‘bundle.js’ }}运行npm run dev生成唯一的打包文件 bundle.js二、多出口形式webpack.config.jsconst path = require(‘path’);module.exports = { //多出口形式 entry:{ entryOne:’./public/entryOne/index.js’, entryTwo:’./public/entryTwo/index.js’, }, output:{ path:path.resolve(__dirname,‘build’), filename:’[name].js’ }}文件结构运行npm run dev生成两个打包文件四、配置webpack-dev-server一、了解 webpack-dev-serverwebpack-dev-server用来配置本地服务器为 webpack 打包生成的文件提供web服务自动刷新和热替换(HMR)二、安装webpack-dev-servernpm install –save-dev webpack-dev-server三、 配置webpack.config.js文件devServer:{ contentBase:’./dist’, //设置服务器访问的基本目录 host:’localhost’, //服务器的ip地址 port:8080, //端口 open:true //自动打开页面}四、配置package.json"scripts": { “start”: “webpack-dev-server –mode development” }五、运行命令npm run dev 打包文件npm run start 打开服务器五、打包css安装loadernpm install style-loader css-loader –save-dev配置loader在webpack.config.js文件里配置module中的rules,如下:test 属性,用于标识出应该被对应的 loader 进行转换的某个或某些文件。use 属性,表示进行转换时,应该使用哪个 loader。module.exports = { /入口和出口文件可以不用配置,默认/ module:{ rules:[ { test:/.css$/, use:[‘style-loader’,‘css-loader’]//引入的顺序至关重要,不可改变 } ] }}测试是否打包成功在src下创建index.css文件在index.js中引入index.css文件require(’!style-loader!css-loader!./index.css’);进行打包后运行 npm run dev(之前配置好,详见第一篇文章:webpack4 基础配置)红色的背景,控制台输出hello————六、使用babel编译ES6babel转化语法所需依赖项:babel-loader: 负责 es6 语法转化babel-core: babel核心包babel-preset-env:告诉babel使用哪种转码规则进行文件处理安装依赖npm install babel-loader @babel/core @babel/preset-env –save-dev配置webpack.config.js文件 { test:/.js$/, exclude:/node_modules/, use:‘babel-loader’ }新建 .babelrc 文件配置转换规则{ “presets”:["@babel/preset-env"]}或者直接在webpack.config.js文件中配置规则 { test:/.js$/, exclude:/node_modules/, use:{ loader:‘babel-loader’, options:{ presets:[’@babel/preset-env’] } } }七、提取分离打包css前面讲过 将css文件引入到js文件中,然后一起打包成js文件,现在我们学习单独提取分离css并且打包。安装插件min-css-extract-pluginnpm install mini-css-extract-plugin –save-dev配置webpack.config.js引入插件 const MiniCssPlugin = require(“mini-css-extract-plugin”);rules 设置 { test:/.css$/, use:[MiniCssPlugin.loader,‘css-loader’] }plugins 设置 new MiniCssPlugin({ filename:’./css/[name].css’ })截图运行命令打包八、压缩优化css压缩css,去除注释安装插件npm install –save-dev optimize-css-assets-webpack-plugin配置webpack.config.js头部引入插件const OptimizeCssAssetsPlugin = require(“optimize-css-assets-webpack-plugin”) ` | 参数 | 意义 | | ————————- | ———————————————————— | | assetNameRegExp | 正则表达式,用于匹配需要优化或者压缩的资源名。默认值是<br/>/.css$/g | | cssProcessor | 用于压缩和优化CSS 的处理器,默认是 cssnano. | | cssProcessorPluginOptions | 传递给cssProcessor的插件选项,默认为{} | | canPrint | 表示插件能够在console中打印信息,默认值是true | | discardComments | 去除注释 |在plugins模块引入 new OptimizeCssAssetsPlugin({ assetNameRegExp:/.css$/g, cssProcessor:require(“cssnano”), cssProcessorPluginOptions:{ preset:[‘default’,{discardComments:{removeAll:true}}] }, canPrint:true })运行打包命令之后就可以压缩webpack –mode development ...

March 6, 2019 · 2 min · jiezi

使用__webpack_nonce__设置CSP的方法

参考来源:https://stackoverflow.com/que…webpack配置,将index.js变为index.ejs模板const HtmlWebpackPlugin = require(‘html-webpack-plugin’)module.exports = { entry: ‘index.js’, output: { path: __dirname + ‘/dist’, filename: ‘index_bundle.js’ }, plugins: [ new HtmlWebpackPlugin({ filename: dirname + ‘/dist/index.ejs’, }) ]}webpack的入口文件(index.js)添加__webpack_nonce = ‘<%=nonce%>’;服务端程序,遇到index.js的请求,就替换掉<%=nonce%>

March 6, 2019 · 1 min · jiezi

Webpack 4 学习02(使用配置文件进行打包)

上一讲中我们打包没有用到webpack.config.js配置文件,webpack4把自己定位为一个零配置的工具。这一讲学习配置文件使用,更好地学习webpack。根目录下新建一个webpack.config.js文件 (记载配置信息)配置文件 const path = require(‘path’); module.exports = { entry:’./public/index.js’, output:{ path:path.resolve(__dirname,‘build’), filename:‘bundle.js’ } }字段意义entry入口,所需打包的文件的路径output出口path文件打包后存放的路径path.solve()将路径或者路径片段的序列处理成绝对路径_dirname表示当前文件所在目录的绝对路径filename打包后文件的名称按照配置项新建一个入口文件public/index.js运行npm run dev

March 6, 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机制

简介以下仅为个人粗略总结和代码,看不懂的稍加理解,本文主要用做个人记录。先大致总结一下1.从哪里开始:webpack根据入口模块开始。2.如何进行:递归读取每个文件,会形成一个依赖列表,依赖列表的,依赖列表是一个以文件相对路径为key,文件内容为value的对象。3.如何处理:对于每个文件会通过AST解析语法树,返回源码。4.loader在哪:loader是何时何地进行处理?它是在读取文件的时候开始起作用,通过正则匹配文件是否需要处理。然后读取配置文件的loader配置,然后通过递归的方式处理文件。5. Plugins呢:plugins是在合适的时机开始工作,那么这个合适时机如何控制呢,是通过tapable事件流机制,实现发布订阅模式。6.最后:最后返回的是一个匿名自执行函数,定义了一个webpack__require方法,解析传入的依赖列表,递归执行。然后看下代码核心代码如下://入口文件webpack.js#! /usr/bin/env node//第一步:找到当前执行命令的路径,拿到webpack.config.jslet path = require(“path”)let config = require(path.resolve(‘webpack.config.js’))console.log(path.resolve(),‘resolve—————>’)let Compiler = require(’../lib/Compiler’)let compiler = new Compiler(config)//标识运行编译compiler.run()//Compiler.jsconst fs = require(‘fs’)const path = require(‘path’)const babylon = require(‘babylon’)const travere = require(’@babel/traverse’).defaultconst t = require(’@babel/types’)const generator = require(’@babel/generator’).defaultconst ejs = require(’ejs’)const {SyncHook} = require(’tapable’)//babylon把源码转为AST//@babel/traverse//@babel/generator//@babel/typesclass Compiler { constructor(config) { this.config = config //保存入口文件路径 this.entryID = ’’ //主模块入口路径 this.modules = {} //存放模块依赖关系 this.entry = config.entry //入口路径 this.root = process.cwd() //当前工作目录 this.hooks = { entryOption: new SyncHook(), compile: new SyncHook(), afterCompile: new SyncHook(), afterPlugins: new SyncHook(), run: new SyncHook(), emit: new SyncHook(), done: new SyncHook(), } //如果传递了plugins参数 let plugins = this.config.plugins if(Array.isArray(plugins)) { plugins.forEach(plugin => { plugin.apply(this) }) } } run() { //执行,并创建模块的依赖关系 this.buildModule(path.resolve(this.root, this.entry), true) //发射一个文件,就是打包后的文件 this.emitFile() } //构建模块 buildModule(modulePath, isEntry) { //首先读取入口文件 let source = this.getSource(modulePath) //模块的ID = this.root - modulePath let moduleName = ‘./’ + path.relative(this.root, modulePath) if(isEntry) { this.entryID = moduleName //保存入口名字 } //解析,需要把source源码进行改造,返回一个依赖列表 let {sourceCde, dependencies } = this.parse(source, path.dirname(moduleName)) this.modules[moduleName] = sourceCde dependencies.forEach(dep => { //附模块的递归加载 this.buildModule(path.join(this.root, dep), false) }) } //解析源码, AST解析语法树 parse(source, parentPath) { // console.log(source, parentPath) let ast = babylon(source) let dependencies = [] //依赖数组 travere(ast, { CallExpression() { let node = p.node if(node.callee.name === ‘require’) { node.callee.name = ‘webpack_require’ let moduleName = node.arguments[0].value //这里就是引用模块的名字 moduleName = moduleName + (path.extname(moduleName) ? ’’ : ‘.js’) moduleName = ‘./’ + path.join(parentPath, moduleName) //‘src/a.js’ dependencies.push(moduleName) node.arguments = [t.stringLiteral(moduleName)] } } }) let sourceCode = generator(ast).code return {sourceCode, dependencies} } //公用读文件的方法 getSource(modulePath) { let content = fs.readFileSync(modulePath, ‘utf8’) let rules = this.config.module.rules //拿到规则 //拿到每个规则来处理 for(let i=0; i<rules.length; i++) { let rule = rules[i] let { test, use } = rule let len = use.length - 1 if(test.test(modulePath)) { //这个模块需要通过loader转换 function normalLoader() { let loader = require(use[len]) //获取对应loader函数 content = loader(content) //递归调用loader if(len >= 0) { normalLoader() } } normalLoader() } } return content } //发射文件 emitFile() { //用数据 渲染我们的 //拿到输出到哪个目录下 let main = path.join(this.config.output.path, this.config.output.filename) //模板路径 let templateStr = this.getSource(path.join(__dirname, ‘main.ejs’)) let code = ejs.render(templateStr, {entryId: this.entryID, modules: this.modules}) this.assets = { //资源中路径对应的代码 } this.assets[main] = code fs.writeFileSync(main, this.assets[main]) }}module.exports = Compiler ...

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

vue+webpack4多页面打包配置

vue+webpack4多页面打包配置多页面配置通常有两种形式,一种是多页面多配置,一种是多页面单配置。因为webpack(3.1以上)可以直接处理一个配置对象的数组,所以可以为每个页面单独写一份配置。 通常来讲,多配置的优点是配置灵活、独立,可以并行打包,从而提高打包速度,缺点是不能在多页面之间共享代码(一个页面加载了之后,下一个页面还得再加载一遍);单配置的特点基本上是和多配置相对。具体使用哪一种形式,看具体业务情况。本文主要介绍的是单配置的形式。 1. 整体目录结构为了便于打包,我们创建一个pages的文件夹,在其下创建一个个的子文件夹代表一个个页面,每个子文件夹中建立各自的spa应用体系,如图所示:这样做的好处是,我们在配置webpack的打包入口时,比较好操作,而且这样的结构也较为清晰。 2. webpack配置2.1 文件结构创建base、dev、prod三个文件。我们在base文件中配置entry、output、loader、公共的plugin等,其余的根据开发环境和线上环境各自所需在各自不同的文件中增删改。2.2 entry根据整体目录结构,每个页面文件夹都有各自的入口js文件,我们在配置entry选项时,就可以按如下编码方式书写:/** * 通过约定,降低编码复杂度 * 每新增一个入口,即在src/pages目录下新增一个文件夹,以页面名称命名,内置一个index.js作为入口文件 * 通过node的文件api扫描pages目录 * 这样可以得到一个形如{page1: “入口文件地址”, page2: “入口文件地址”, …}的对象 /const getEntries = () => { let result = fs.readdirSync(pagesDirPath); let entry = {}; result.forEach(item => { entry[item] = path.resolve(__dirname, ../src/pages/${item}/index.js); }); return entry;}module.exports = { entry: getEntries() …}2.3 outputoutput的配置选项如下,打完包后的目录结构如图所示://判断是否是开发环境const devMode = process.env.NODE_ENV === “development”; module.exports = { … output: { publicPath: devMode ? "" : “/”, //这里的name即为我们entry对象中的每一个key值,也就是我们在pages目录下创建的一个个文件夹的名称 filename: devMode ? “[name].js” : “static/js/[name].[chunkhash].js”, path: path.resolve(__dirname, “../dist”) } …}2.4 html-webpack-plugin配置完了entry和output,接下来需要为每个页面生成一个单独的html文件,也就是为每个页面创建一个html-webpack-plugin的实例:/* * 扫描pages文件夹,为每个页面生成一个插件实例对象 */const generatorHtmlWebpackPlugins = () => { let arr = []; let result = fs.readdirSync(pagesDirPath); result.forEach(item => { //判断页面目录下有无自己的index.html let templatePath; let selfTemplatePath = pagesDirPath + /${item}/index.html; let publicTemplatePath = path.resolve(__dirname, “../src/public/index.html”); try { fs.accessSync(selfTemplatePath); templatePath = selfTemplatePath; } catch(err) { templatePath = publicTemplatePath; } arr.push(new HtmlWebpackPlugin({ template: templatePath, filename: ${item}.html, chunks: [“manifest”, “vendor”, item] })); }); return arr;}module.exports = { … plugins: [ …generatorHtmlWebpackPlugins() ] …}这里为了灵活性考虑,判断了各自的页面子文件夹中有无html模板文件;如果不需要,可以把templat路径直接定义成公共html文件的地址。2.5 其他配置基本上前面的几点配置就是一个多页面打包配置的雏形。此外还可以配置下诸如optimization、mini-css-extract-plugin等生产环境打包的优化配置。在文末的github地址中可以看到全部的配置信息。3. 多页面+SPA虽然我们这是一个多页面的应用,但每个页面也可以做成一个spa,如果你有这种需求的话;此外可以配置@babel/plugin-syntax-dynamic-import插件以支持import(),在router层面做代码分割和懒加载。附原文代码地址:https://github.com/gww666/2-m… ...

March 4, 2019 · 1 min · jiezi

webpack and react按需加载

原理:CommonJS与import()方法一:CommonJS模块语法利用require.ensure,require.ensure()是webpack特有的,已经被import()取代。方法require.ensure( dependencies: String[], callback: function(require), errorCallback: function(error), chunkName: String)方法二:import()ES2015 loader规范定义了import()方法,可以在运行时动态地加载ES2015模块方法import(‘Component’).then()// or 在 async中使用await import(‘Component’)demoimport React, { Component } from ‘react’;class App extends Component { handleClick = () => { import(’./moduleA’) .then(({ moduleA }) => { // Use moduleA }) .catch(err => { // Handle failure }); }; render() { return ( <div> <button onClick={this.handleClick}>Load</button> </div> ); }}export default App;react-router中使用按需加载demo地址,此处配合create-react-app使用,自己配置webpack合理需要配置output.fileName和output.chunkFilename方法一:使用react.lazyimport { BrowserRouter as Router, Route, Switch } from ‘react-router-dom’;import React, { Suspense, lazy } from ‘react’;const Program1 = lazy(() => import(’./Program1’));const App = () => ( <Router> <Suspense fallback={<div>Loading…</div>}> <Switch> <Route path="/program1" component={Program1}/> </Switch> </Suspense> </Router>);查看代码方法二:利用高阶组件写一个高阶组件用于动态加载组件// async Componentimport React, { Component } from “react”;export default function asyncComponent(importComponent) { class AsyncComponent extends Component { constructor(props) { super(props); this.state = { component: null }; } async componentDidMount() { const { default: component } = await importComponent(); this.setState({ component: component }); } render() { const C = this.state.component; return C ? <C {…this.props} /> : null; } } return AsyncComponent;}查看代码引用并使用该高阶组件做按需加载import { BrowserRouter as Router, Route, Switch } from ‘react-router-dom’;import asyncComponent from ‘./asyncComponent’;import React, { Suspense } from ‘react’;const Progran2 = asyncComponent(() => import("./Program2"));const App = () => ( <Router> <Suspense fallback={<div>Loading…</div>}> <Switch> <Route exact path="/program2" component={Program2}/> </Switch> </Suspense> </Router>);查看代码以上两种方法都是react官方推荐code-splitting ...

March 4, 2019 · 1 min · jiezi

一文读懂 babel7 的配置文件加载逻辑

近期,在波洞星球的PC官网项目中,我们采用了新版的 babel7 作为 ES 语法转换器。而 babel7 中的一大变更就是对配置文件的加载逻辑进行了改进,然而实际上对于不熟悉 babel 配置逻辑的朋友往往会带来更多问题。本文就是 babel7 配置文件的中文指南,它是英语渣渣的救星,是给懒人送到口边的一道美味。如有错误 概不负责 欢迎指正。前言babel7 从 2018年3月开始进入 alpha 阶段,时隔5个月直到 2018年8月份 release 第一个版本,目前的最新版是2019年2月26号发布的 7.3.4. 时光如梭,在这美好的 9012 年,ES2019 都快要发布了的时刻,我想: 是时候用一用 babel7 了。本文不是 babel7 的升级教程,而是对 babel7 的新变化和配置逻辑的一点心得。babel7 对monorepo 结构项目的优化恰好符合我们目前项目架构的预期,这简化了我们配置的复杂度,但其难以理解的配置加载逻辑,却让我踩了不少坑,这也正是本文的来源。说点变化在开始讲 babel7 的配置逻辑之前,我们先从以下几个方面来啰嗦几句 babel7 所做的变更及其逻辑意义。proposal 语法特性在历史上(babel6)的时代,人们通常使用 babel 提供的 preset-stage 预设来体验 ES6 之后的处于建议阶段的语法特性。例如做如下的 babel 配置:“presets”: [“es2015”, “react”, “stage-0”]其中,es2015 预设会包含 ES6 标准中所有语法特性;stage-0预设会包含当前(安装该预设npm包的时刻) 的 ES 语法进展中的 stage 0到3的特性(数字小的包含数字大的)。但事实上 babel 官方这样提供 stage 预设,会有不少问题例如:随着 es 标准的不断发展,大量的新特性几乎已经成为标准。与此同时,stage0-3阶段的特性必然也发生变化。可以说,stage0-3的阶段特性他们是不稳定的,极有可能在某个时机被TC39委员会除名、变更阶段、改变语法。尽管 babel-preset-* 预设会跟随TC39 保持一致的更新, 但这样的用法需要使用者也不断保持更新 才能跟标准一致历史上的 preset-es2015 配合 preset-stage-0 的做法极易产生疑惑,例如没有人知道他所需要的特性在stage几一个语言特性如果从 stage3 变更为 stage4,往往会导致以前的 stage0(包含了1、2、3) 的配置出问题。因为特性推进后,新的stage0中就不再包含该特性内容,但使用者可能不知道要把该特性所在的 ES标准 加入到配置中大量的社区工具 eslint 等等都依赖 babel;babel 的 preset-stage 预设更新就会导致这些社区工具频频出现问题。如今,babel 官方认为,把不稳定的 stage0-3 作为一种预设是不太合理的,因此废弃了 stage 预设,转而让用户自己选择使用哪个 proposal 特性的插件,这将带来更多的明确性(用户无须理解 stage,自己选的插件,自己便能明确的知道代码中可以使用哪个特性)。所有建议特性的插件,都改变了命名规范,即类似 @babel/plugin-proposal-function-bind 这样的命名方式来表明这是个 proposal 阶段特性。ES 标准特性对于正经的 ES 标准特性,babel从6开始就建议使用 babel-preset-env 这个能根据环境进行自动配置的预设。到了 babel7,我们就可以完全告别这几个历史预设了: preset-es2015/es2016/es2017/latest为什么 preset-env 要更好呢?我认为,对于开发者而言,关注目标用户平台(兼容哪些浏览器)要比关注 “编译为哪份ES标准” 要更易理解。把选择编译插件的事情交给 preset-env 就好了。它会根据 compat table 和你设置的目标用户平台选择正确的插件。polyfill跟 stage 预设的结局一样,对于处于建议阶段的特性,polyfill里面也移除了对他们的支持。以前的 babel-polyfill 是这么实现的:import “core-js/shim”; // included < Stage 4 proposals import “regenerator-runtime/runtime"现在的 @babel/polyfill 就直接引入 core-js v2 的属于ES正式标准的模块。这意味着,如果你需要使用处于 proposal 阶段的语法特性,你需要手工 import core-js 中的对应模块。命名空间从 babel7 开始,所有的官方插件和主要模块,都放在了 @babel 的命名空间下。从而可以避免在 npm 仓库中 babel 相关名称被抢注的问题。有必要说一下的,比如 @babel/node @babel/core @babel/clil @babel/preset-envtransform-runtime以前的 babel-transform-runtime 是包含了 helpers 和 polyfill。而现在的 @babel/runtime 只包含 helper,如果需要 polyfill,则需主动安装 core-js 的 runtime版本 @babel/runtime-corejs2 。并在 @babel/plugin-transform-runtime 的插件中做配置。说重点: 配置这是本文的重点,先来看一段 babel7 对配置的变更说明Babel has had issues previously with handling node_modules, symlinks, and monorepos. We’ve made some changes to account for this: Babel will stop lookup at the package.json boundary instead of looking up the chain. For monorepo’s we have added a new babel.config.js file that centralizes our config across all the packages (alternatively you could make a config per package). In 7.1, we’ve introduced a rootMode option for further lookup if necessary.段落的意思大概有这么几点:Babel将停止在package.json边界查找而不是查找链。译者注:这说明以前babel会递归向上查找babelrc 而现在检索行为会停在package.json所在层级。这可以解决部分符号链接的js向上查找babelrc错乱的问题。添加了一个新的项目全局babel.config.js文件,可以将整个项目的配置集中在所有包中。译者注:除了新增的这个全局配置,也可以同时支持以前的基于文件的.babelrc的配置引入了一个rootMode选项,以便在必要时按一定策略查找 babel.config.js除此之外,babel7 还有一个特性是:默认情况下,不会加载monorepo项目的任何独立子项目中的 .babelrc 文件然而,对上面的解释,你可能: 每个字都认识,连在一起却不知道在说什么。下面我们来剖析一下概念为了理解 babel7 的配置逻辑,我们就以 babel7 真正所解决的痛点 [monorepo 类型的项目] 为例来剖析。在此之前,我们需要预先确定几个概念。monorepo。这是个自造词。按我的理解,它的含义是说 单个大项目但是包含多个子项目 的含义。如果还是不能理解的话,就把 项目 二字 换成 npm模块包 (以package.json文件作为分界线)。即 单个npm包中又包含多个子npm包 的项目。 例如,波洞的 PC 版采用的是 Node.js 作为前端接入层的方式,在我们的项目结构组织上,是这样的:|- backend |-package.json|- frontend |-package.json|- node_modules|- config.js|- babel.config.js|- package.json这就是典型的 monorepo 结构。全局配置。在 babel 文档中又叫 项目级别的配置,特指 babel.config.js。如上图的monorepo结构,其 babel.config.js 就是全局配置/项目配置,该 babel 配置对 backend、frontend、甚至 node_modules 中的模块全部生效。局部配置。在 babel 文档中可能叫 相对于文件的配置。这种配置就是特指的 .babelrc 或 .babelrc.js 了。他们的生效范围是与待编译文件的位置有关的。规则懂了几种配置文件的概念和作用范围之后,我们就可以来根据文档和代码测试结果来精确描述 babel7 的配置规则。这里我们直接以 monorepo 类型项目为例来说,因为普通项目会更简单。下文中可能用到的名词解释:我们用 package 来代指一个具有独立 package.json 的项目,如上面案例中的 frontend 可以称作一个 package,backend也可以称作一个package; 我们用 相对配置 这个名词来表达所谓的 .babelrc 和 .babelrc.js,用全局配置来代指 babel.config.js这份配置对monorepo类型项目,babel7 的处理逻辑是:【全局配置】全局配置 babel.config.js 里的配置默认对整个项目生效,包括node_modules。除非通过 exclude 配置进行剔除。【全局配置】全局配置中如果没有配置 babelrcRoots 字段,那么babel 默认情况下不会加载任何子package中的相对配置(如.babelrc文件)。除非在全局配置中通过 babelrcRoots 字段进行配置。【全局配置】babel 全局配置文件所在的位置就决定了你的项目根目录在哪里,默认就是执行babel的当前工作目录,例如上面的例子,你在根目录执行babel,babel才能找到babel.config.js,从而确定该monorepo的根目录,进而将配置对整个项目生效【相对配置】相对配置可被加载的前提是在 babel.config.js 中配置了 babelrcRoots. 如 babelrcRoots: [’.’, ‘./frontend’],这表示要对当前根目录和frontend这个子package开启 .babelrc 的加载。(注意: 项目根目录除了可以拥有一个 babel.config.js,同时也可以拥有一个 .babelrc 相对配置)【相对配置】相对配置加载的边界是当前package的最顶层。假设上文案例中要编译 frontend/src/index.js 那么,该文件编译时可以加载 frontend 下的 .babelrc 配置,但无法向上检索总项目根目录下的 .babelrc实战还是以上面的代码结构为例。|- backend |-package.json|- frontend |-package.json|- node_modules|- config.js|- babel.config.js|- package.json该案例中,我们思考发现,我们需要利用 babel7 的全局配置能力。原因在于,monrepo 中存在多个 子 package。由于 babel7 默认检索 babelrc 的边界是 当前package。因此每个package中撰写的babelrc只会对当前package生效,这会导致我们的frontend中依赖根目录的config.js时无法得到正确的编译;另一个问题是: frontend和backend中的相同的babel配置部分无法共享 存在一定冗余。为此,我们需要在项目根目录设置一个 babel.config.js的配置,用它再配合babelrc来做babel配置的共享和融合。但是,问题很快来了:当工作目录不在根目录时,无法加载到全局配置。我们的前端编译脚本通常放置在 frontend目录下,(我们执行编译的工作目录是在 frontend 中),此时 babel build 行为的 工作目录 便是 frontend. 由于 babel 默认只在当前目录寻找 babel.config.js 这个全局配置,因此会导致无法找到根目录的 babel.config.js,这样我们所设想的整个项目的全局配置就无法生效。 幸好,babel7 提供了 rootMode 选项,可以将它指定为 “upward”, 这样babel 会自动向上寻找全局配置,并确定项目的根目录位置。设置方法:CLI: babel –rootMode=upwardwebpack: 在 babel-loader 的配置上设置 rootMode: ‘upward’现在,全局配置有了,我们可以在里面配置 babel 转译规则,它可以对全项目生效,frontend下的 vue.js 编译自然没有问题了。不过,假设我们 backend 项目中也要使用 babel 转译(目前我们实际在 backend 中并没有使用,因为我们认为只图esmodule而多加一层编译得不偿失),那么必然 backend 与 frontend 中的编译配置是不同的,frontend 需要加载 vue 的 jsx 插件和polyfill (useBuiltIns: usage,modules: false),而backend只需要转译基本模块语法(modules: true, useBuiltIns: false)。该场景的解决方案便是,为每个子 package 提供独立的 .babelrc 相对配置,在全局 babel.config.js 中设置共用的配置。此时项目组织结构如下:|- backend |- .babelrc.js |-package.json|- frontend |- .babelrc.js |-package.json|- node_modules|- config.js|- .babelrc.js // 这份配置在本场景下不需要(如果根目录下的代码有区别于子package的babel配置,则需要使用)|- babel.config.js|- package.json根目录的 babel.conig.js 配置应该如下:const presets = [ // 根、frontend、backend 的公共预设]const plugins = [ // 根、frontend、backend 的公共插件]module.exports = { presets, plugins, babelrcRoots: [’.’, ‘./frontend’, ‘./backend’] // 允许这两个子 package 加载 babelrc 相对配置}以为此时已经高枕无忧了?navie,由于我们前端 Vue.js 采用 webpack 打包。实际开发过程中发现,这种配置会造成 webpack 打包模块时出现故障,故障原因在于:同一个模块中错误混用 esmodule 和 commonjs 语法会造成 webpack故障。 前文讲到 全局配置 global.config.js 会作用到 整个项目,甚至包括 node_modules。因此babel编译时会同时编译 node_modules 下的模块,虽然模块作者不可能在一个js文件中混用不同模块语法,但他们作为释出包 通常是commonjs的模块语法。 而preset-env预设在编译时会通过 usage 方式 默认注入import语法的 polyfillSince Babel defaults to treating files are ES modules, generally these plugins/presets will insert import statements这便是蛋疼的来源:babel加载过的node_modules模块会变成 同一个js文件里既有commonjs语法又有esmodule语法。解决方案:不要对 node_modules 下的模块采用babel编译。我们需要在 babel.config.js 配置中增加选项:exclude: /node_modules/总结至此,我们的 monorepo 项目就可以使用一份 全局配置+两份相对配置,实现分别对 前端和后端 进行合理的ES6+语法的编译了。这是我们配置工程师的一小步,但是前端走向未来语法的一大步。总结 babel7 的配置加载逻辑如下:babel.config.js 是对整个项目(父子package) 都生效的配置,但要注意babel的执行工作目录。.babelrc 是对 待编译文件 生效的配置,子package若想加载.babelrc是需要babel配置babelrcRoots才可以(父package自身的babelrc是默认可用的)。任何package中的babelrc寻找策略是: 只会向上寻找到本包的 package.json 那一级。node_modules下面的模块一般都是编译好的,请剔除掉对他们的编译。如有需要,可以把个例加到 babelrcRoots 中。虽然写的很乱,但您有收获吗,有的话点个赞吧.或许你还没有看明白。没关系,知道最终的配置代码怎么粘贴就好了~ ...

March 1, 2019 · 3 min · jiezi

使用 webpack 构建应用

如何使用webpacknpm init -ynpm install webapck webpack-cli –save-devtouch webpack.config.js在webpack.config.js中下面添加内容const path = require(‘path’);module.exports = { entry: ‘./src/index.js’, output: { filename: ‘main.js’, path: path.resolve(__dirname, ‘dist’) }};entry:工程资源的入口,可以是单个文件,也可以是多个文件,通过每一个资源入口,webpack会一次去寻找它的依赖进行模块打包。我们可以把entry理解为整个依赖树的根,每个入口都将对应一个最终生成的打包结果。output:这是一个配置对象,通过它我们可以对最终打包的产物进行配置,这里配置了两个属性,:path:打包资源放置的路劲,必须为绝对路径。filename:打包结果的文件名。定义好配置文件后,用npx webpack或者./node_modules/.bin/webpack执行使用loader项目中需要引入一个css文件,如果直接用webpack去执行就会报错,需要再webpack中加入loader机制module.exports = { … module: { rules: [ { // 用正则去匹配 .css 结尾的文件,然后需要使用 loader 进行转换 test: /.css$/, use: [‘style-loader’, ‘css-loader’] } ] }}编译之前还需要安装style-loader和css-loadernpm install –save-dev style-loader css-laoder注意:use属性的值是一个使用loader名称组成的数组,loader执行的顺序是从后往前的,由于loader执行有顺序,故不能写成这样use: [‘css-loader’, ‘style-loader’]每个loader都可以通过URL queryString的方式传入参数,比如’css-loader?url’style-loader的原理:是将css的内容使用javascript的字符串存储起来,在网页执行javascript时通过DOM操作,动态地向HTML head标签里插入HTML style标签。配置loader的方式也可以用Object来实现use: [‘style-loader’, { loader: ‘css-loader’, options: { url: true }}]使用pluginloader的作用是被用于转换某些类型的模块,而插件则可以用于执行范围更广的任务,插件的范围包括,从打包优化和压缩,一直到重新定义环节中的变量。如果想要使用一个插件,我们只需要require()它,然后把它添加到plugins数组中。我们可以在一个配置文件中因为不同的目的多次使用用一个插件,因此我们可以使用new操作符来创建它的实列。上面使用loader把css加载到js中去,现在使用extract-text-webpack-plugin插件把bundle.js文件里的css提取到单独的文件中// 提取 css 的插件const ExtractTextPlugin = require(’extract-text-webpack-plugin’)module: { rules: [ { // 用正则去匹配 .css 结尾的文件,然后需要使用 loader 进行转换 test: /.css$/, loaders: ExtractTextPlugin.extract({ //转换 .css需要使用的 loader use: [‘css-loader’] }) } ]},plugins: [ //从 js 文件中提取出来的 .css 文件名称 new ExtractTextPlugin({ filename: ‘main.css’ })]编译之前需要安装extract-text-webpack-pluginnpm install –save-dev extract-text-webpack-plugin执行webpack时报错需要这样安装npm install extract-text-webpack-plugin@nextDevServer官方网站安装npm install webpack-dev-server –save-dev然后进行简单的配置devServer:{ port: 3000, publicPath: “/dist”}然后用./node_modules/.bin/webpack-dev-server执行资源压缩npm i uglifyJSPlugin-webpack-plugin –save-dev配置文件const UglifyJSPlugin = require(‘uglifyjs-webpack-plugin’)plugins: [ new UglifyJSPlugin({ //默认是 false 需要手动开启 parallel: true })]或者optimization: { minimizer: [new UglifyJsPlugin()],},按需加载在代码层面,webpack支持两种方式进行异步模块加载,一种是CommonJS形式的require.ensure,一种是ES6 Module形式的异步import()异步加载的脚本不允许使用document.write,所以将module.js的代码改成console.logexport const log = function(){ console.log(‘module.js loaded.’)}编辑app.js,将module.js以异步的形式加载进来import(’./module.js’).then(module =>{ module.log()}).catch(error => “An error occurred while loading the module”)document.write(‘app.js loaded.’)修改配置module.exports = { mode: “production”, entry: ‘./app.js’, output: { filename: ‘main.js’, path: path.resolve(__dirname, ‘dist’), publicPath: “./dist” },}这里我们在output中添加了一个配置项publicPath,它是webpack中一个很重要有很容易引起迷惑的配置,当我们的工程中有按需加载以及图片和文件等外部资源时,就需要它来配置这些资源的路径,否则页面上就会报404,这里我们将publicPath配置为相对于html的路径,使按需加载的资源生产在dist目录下,并且能正确地引用到它。重新打包之后你会发现打包结果中多出一个1.mian.js,这里面就是将来会被异步加载进来的内容。刷新页面并查看chrome的network标签,可以看到页面会请求1.main.js。它并不来源于index.html中的引用,而是通过main.js在页面插入了script标签来将其引入的。使用webpack的构建特性从2.0.0版本开始,webpack开始加入了更多的可以优化构建过程的特性。tree-shaking在关于模块的那一篇文章中我们提到过,ES6 Module的模块依赖解析是在代码静态分析过程中进行的。换句话说,它可以在代码的编译过程中得到依赖树,而非运行时。利用这一点webpack提供tree-shaking功能,它可以帮助我们检测工程中哪些模块有从未被引用到的代码,这些代码不可能被执行到,因此也称为“死代码”。通过tree-shaking,webpack可以在打包过程中去掉这些死代码来减小最终的资源体积。开启tree-shaking特性很简单,只要保证模块遵循ES6 Module的形式定义即可,这意味着之前所有我们的例子其实都是默认已经开启了的。但是要注意如果在配置中使用了babel-preset-es2015或者babel-preset-env,则需要将其模块依赖解析的特性关掉,如:presets: [ [env, {module: false}]] 这里我们测试一下tree-shaking的功能,编辑module.js:// module.js export const log = function() { console.log(‘module.js loaded.’); } export const unusedFunc = function() { console.log(’not used’); } 打开页面查看1.main.js的内容,应该可以发现unusedFunc的代码是不存在的,因为它没有被别的模块使用,属于死代码,在tree-shaking的过程中被优化掉了。tree-shaking最终的效果依赖于实际工程的代码本身,在我对于实际工程的测试中,一般可以将最终的体积减小3%~5%。总体来看,我认为如果要使tree-shaking发挥真正的效果还要等几年的时间,因为现在大多数的npm模块还是在使用CommonJS,因此享受不了这个特性带来的优势。scope-hoistingscope-hoisting(作用域提升)是由webpack3提供的特性。在大型的工程中模块引用的层级往往较深,这会产生比较长的引用链。scope-hoisting可以将这种纵深的引用链拍平,使得模块本身和其引用的其它模块作用域处于同级。这样的话可以去掉一部分 webpack的附加代码,减小资源体积,同时可以提升代码的执行效率。目前如果要开启scope-hoisting,需要引入它的一个内部插件:module.exports = { plugins: [ new webpack.optimize.ModuleConcatenationPlugin() ] }scope-hoisting生效后会在bundle.js中看到类似下面的内容,你会发现log 的定义和调用是在同一个作用域下了:// CONCATENATED MODULE: ./module.js const log = function() { console.log(‘module.js loaded.’); } // CONCATENATED MODULE: ./app.js log(); ...

March 1, 2019 · 2 min · jiezi

webpack4配置Vue多页面入口轻量级模板

前言之前写过一次关于webpack配置多页面应用,写的不是很好,这次项目要用到多页面应用,于是重新基于webpack4构建了一套关于vue的多页面应用。我在网上搜索了一圈,发现vue多页面配置,大部分都是基于vue-cli配置的,很少是从基础开始配置,如是我通过webpack4,构建了一个提供多页面入口,打包,调试的轻量级的构建工具,不依赖过多配置,只加载常用的配置,用更少的代码,做更多的东西项目结构├── build // webpack配置目录│ ├── webpack.config.base.js // 公共配置│ ├── webpak.config.dev.js // 开发模式│ ├── webpak.config.prod.js // 打包模式├── dist // 项目打包路径(自动生成)├── page // 多页面入口(自定义)├── public // index.html模板├── src // 源码目录(自定义)├── postcss.config // 样式添加前缀├── pages.js // 多页面配置项项目运行克隆项目git clone git@github.com:hangjob/vue-multiple-webpack4-template.git安装依赖npm install 或 yarn开发模式npm run dev里面已经写好了两个入口文件,启动后可直接访问http://localhost:3000/home.htmlhttp://localhost:3000/login.html打包模式npm run build打包后生成文件dist目录文件解释关于build中使用的插件项在文件配置后面注释写的都很清楚多页面配置项(pages.js)pages: [ { page: ‘home’, entry: path.resolve(__dirname, ‘./page/home.js’), //指向入口文件 title: ‘这是页面1’, template: path.resolve(__dirname, ‘./public/index.html’), //指向模板文件 filename: ‘home.html’, chunks: [‘home’,‘common’], // 引入公共模块common }, { page: ’login’, entry: path.resolve(__dirname, ‘./page/login.js’), //指向入口文件 title: ‘这是页面2’, template: path.resolve(__dirname, ‘./public/index.html’), //指向模板文件 filename: ’login.html’, chunks: [’login’], }]webpack.config.dev.js 开发模式mode: ‘development’,devtool: ‘cheap-module-eval-source-map’,// 原始代码(只有行内),但是更高的质量和更低的性能watch: true,watchOptions: { poll: 1000, //每秒监控讯问次数 aggregateTimeout: 500, //防抖 ignored: ‘/node_modules/’ //忽略监控文件 },devServer:{ port: 3000, hot: true, progress: false, //记录条 contentBase: path.resolve(__dirname, ‘../public’), //表示的是告诉服务器从哪里提供内容 compress: true //开启gzip压缩}webpack.config.prod.js 生产模式ode: ‘production’,devtool: ‘cheap-module-source-map’,// 原始代码(只有行内)每行代码从loader中进行映射plugins: [ new CleanWebpackPlugin([‘dist’], { root: path.resolve(__dirname, ‘..’), dry: false // 启用删除文件 })],optimization: { minimizer: [ new UglifyJsPlugin({ cache: true, parallel: true, //启用缓存并且启用多进程并行运行 sourceMap: true //错误消息位置映射(减慢编译速度),线上错误方便查看 }), new OptimizeCSSAssetsPlugin({}) ]}webpack.config.base.js 公共模块optimization: { splitChunks: { cacheGroups: { // 将 node_modules目录下被打包的代码到common/common.js common: { test: /node_modules/, chunks: “initial”, //只对入口文件处理 name: “common”, //配置公共模块名称 minChunks: 2, //表示被引用次数,默认为1,比如在项目中有2处引用到一样的模块就会抽离到公共模块下 maxInitialRequests: 5, // 最大的初始化加载次数,默认为1 minSize: 0 //表示在压缩前的最小模块大小,默认为0 } } }} 网络下载太慢,请使用淘宝镜像1.临时使用npm –registry https://registry.npm.taobao.org install express2.持久使用npm config set registry https://registry.npm.taobao.org配置后可通过下面方式来验证是否成功npm config get registry 或者 npm info express3.通过cnpm使用npm install -g cnpm –registry=https://registry.npm.taobao.org说明github地址,后面会持续更新,如果对您有帮助,您可以点右上角 “Star” 支持一下 谢谢! ^_^ 如要在编译过程中遇到错误,点击联系作者感谢这些文章提供帮助项目有使用到这些文章的我都注释过webpack中的path、publicPath和contentBasemini-css-extract-pluginwebpack4 splitChunksPlugin && runtimeChunkPlugin 配置杂记Vue Loader ...

March 1, 2019 · 1 min · jiezi

Webpack4 学习笔记 - 03:loader 打包静态资源(样式)

使用 style-loader,css-loader 打包 css 文件首先在src目录下创建一个 index.css 文件,内容如下:.this_style { color: red;}修改 index.js 文件内容如下:import ‘./index.css’;var root = document.getElementById(‘root’);var wp = document.createElement(‘div’);wp.innerText = ‘Hello, Webpack’;wp.classList.add(’this_style’);root.append(wp);代码的内容是创建一个div,并把this_style的样式赋给它,使它的字体变为红色。然后直接运行 npm run bundle 来打包会报错吗?当然会了,因为之前说过 webpack 只能处理 js 文件,遇到 css 文件时,它就不知道该怎么办了。所以我们就配置一下 webpack.config.js 来告诉它怎么做,配置内容如下:const path = require(‘path’); // 得到的path为webpack.config.js所在的目录module.exports = { entry: { main: ‘./src/index.js’ }, output: { filename: ‘bundle.js’, path: path.resolve(__dirname, ‘dist’) }, module: { rules: [{ test: /.css$/, use: [‘style-loader’, ‘css-loader’] }] }, mode: ‘development’}然后安装 style-loader 和 css-loader:npm install style-loader css-loader -D安装完之后,执行打包命令,没有报错,就说明 webpack 已经正确打包好了 css 文件。打开 index.html 可以看到,字体的颜色已经变成了红色:下面大概来说一下 style-loader 和 css-loader 做了哪些工作,详细的说明可以去看官方文档。在src目录下再新建一个 test.css 文件,给 this_style 样式加一个背景色:.this_style { background-color: #999999; /* 灰色 */}然后在 index.css 文件中引入 test.css:@import ‘./test.css’;.this_style { color: red;}执行打包命令,打开 index.html 查看结果:打包成功, 背景色显示了出来。在这段打包的过程中,css-loader 会根据 css 的语法,比如 @import.. 分析出几个 css 文件的关系,然后把它们合并成一段 css,style-loader 在得到 css-loader 生成的内容之后,会把这段内容添加到指定的内页元素上,最后呈现出上图的结果。 ...

February 28, 2019 · 1 min · jiezi

Webpack4 学习笔记 - 02:loader

什么是 loader?看官网的解释:loader 让 webpack 能够去处理那些非 JavaScript 文件(webpack 自身只理解 JavaScript)。loader 可以将所有类型的文件转换为 webpack 能够处理的有效模块,然后你就可以利用 webpack 的打包能力,对它们进行处理。下面就通过例子来演示。向src文件夹中添加一张图片 triss.jpg:然后修改index.js文件中的代码:const Header = require(’./header.js’);const triss = require(’./triss.jpg’); // 增加这一行new Header();运行 npm run bundle 来打包,会发现报错了:错误提示说triss.jpg这个文件打包出了问题,这是因为 webpack 只能处理 js 文件,遇到 jpg 文件,它就懵逼了,不知道该怎么处理了,然后就报错了。想让它处理非 js文 件该怎么办呢?这就要用到 loader 了!

February 28, 2019 · 1 min · jiezi

Gulp和webpack的区别

背景:今天了解了大厂前端任职的一些要求:里面写了要熟悉webpack打包工具和Gulp构建工具,作为前端新人,看到这个就想这两种工具有什么区别?在这里解答一下自已,而且希望可以帮助到额疑问的人.Gulp构建工具gulp是工具链,构建工具,可以配合各种插件做JS压缩,CSS压缩,less编译替代手机实现自动化工作.所以它的主要作用是1.构建工具2.自动化3.提高效率Webpack打包工具web是文件打包工具,可以把项目的各种js文件,css文件等打包合成一个或多个文件,主要用于模块化方案,预编译模块的方案所以它的主要作用是1.打包工具2.模块化识别3.编译模块代码方案当然也会有部分相似的功能,比如合并,区分.本文内容转载自此链接,可点击跳转.推荐一篇gulp入门的博文,点击可马上开始学习,作者的博客很给力喜欢

February 28, 2019 · 1 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-cli中使用flexible和px2rem-loader

1.下载lib-flexiblenpm i lib-flexible –save或yarn add lib-flexible2.在项目中引入lib-flexible 一般情况在(main.js中引入lib-flexible)import ’lib-flexible/flexible'3.设置meta标签(视情况而定)<meta name=“viewport” content=“width=device-width, initial-scale=1.0”>4.安装px2rem-loadernpm install px2rem-loader –save或yarn add px2rem-loader5.在webpack中配置px2rem-loader1,在项目build文件中找到util.js,将px2rem-loader添加到cssLoaders中,2,px2rem-loader 下的 remUnit 根据设计稿而定具体如下:exports.cssLoaders = function (options) { options = options || {} const cssLoader = { loader: ‘css-loader’, options: { minimize: process.env.NODE_ENV === ‘production’, sourceMap: options.sourceMap } } //rem编译(新加px2) const px2remLoader = { loader: ‘px2rem-loader’, options: { remUnit: 75 // 由于设计稿是750 } } function generateLoaders (loader, loaderOptions) { var loaders = [cssLoader,px2remLoader]//添加到loader里面 省略….. }}6.项目中使用假设设计稿width和font-size分别是200px,50px 具体代码:(按照设计稿写即可).demo{ width: 200px; font-size: 50px;}7.单独使用flexible可直接添加一个rem.js文件,具体如下:1,配合less,sass和stylus来做px转换rem2,具体可参看css编译/*** 移动端自适应方案 FROM 手淘*/export const setViewport = (()=>{//此处是单独使用 const win = window const lib = window.lib || (window.lib = {})//;(function(win, lib) {//此处是直接使用 let doc = win.document; let docEl = doc.documentElement; let metaEl = doc.querySelector(‘meta[name=“viewport”]’); let flexibleEl = doc.querySelector(‘meta[name=“flexible”]’); let dpr = 0; let scale = 0; let tid; let flexible = lib.flexible || (lib.flexible = {}); if (metaEl) { //console.warn(‘将根据已有的meta标签来设置缩放比例’); let match = metaEl.getAttribute(‘content’).match(/initial-scale=([\d.]+)/); if (match) { scale = parseFloat(match[1]); dpr = parseInt(1 / scale); } } else if (flexibleEl) { let content = flexibleEl.getAttribute(‘content’); if (content) { let initialDpr = content.match(/initial-dpr=([\d.]+)/); let maximumDpr = content.match(/maximum-dpr=([\d.]+)/); if (initialDpr) { dpr = parseFloat(initialDpr[1]); scale = parseFloat((1 / dpr).toFixed(2)); } if (maximumDpr) { dpr = parseFloat(maximumDpr[1]); scale = parseFloat((1 / dpr).toFixed(2)); } } } if (!dpr && !scale) { //let isAndroid = win.navigator.appVersion.match(/android/gi); let isIPhone = win.navigator.appVersion.match(/iphone/gi); let devicePixelRatio = win.devicePixelRatio; if (isIPhone) { // iOS下,对于2和3的屏,用2倍的方案,其余的用1倍方案 if (devicePixelRatio >= 3 && (!dpr || dpr >= 3)) { dpr = 3; } else if (devicePixelRatio >= 2 && (!dpr || dpr >= 2)){ dpr = 2; } else { dpr = 1; } } else { // 其他设备下,仍旧使用1倍的方案 dpr = 1; } scale = 1 / dpr; } docEl.setAttribute(‘data-dpr’, dpr); if (!metaEl) { metaEl = doc.createElement(‘meta’); metaEl.setAttribute(’name’, ‘viewport’); metaEl.setAttribute(‘content’, ‘initial-scale=’ + scale + ‘, maximum-scale=’ + scale + ‘, minimum-scale=’ + scale + ‘, user-scalable=no’); if (docEl.firstElementChild) { docEl.firstElementChild.appendChild(metaEl); } else { let wrap = doc.createElement(‘div’); wrap.appendChild(metaEl); doc.write(wrap.innerHTML); } } function refreshRem(){ let width = docEl.getBoundingClientRect().width; if (width / dpr > 540) { width = 540 * dpr; } let rem = width / 10; docEl.style.fontSize = rem + ‘px’; flexible.rem = win.rem = rem; } win.addEventListener(‘resize’, function() { clearTimeout(tid); tid = setTimeout(refreshRem, 300); }, false); win.addEventListener(‘pageshow’, function(e) { if (e.persisted) { clearTimeout(tid); tid = setTimeout(refreshRem, 300); } }, false); if (doc.readyState === ‘complete’) { doc.body.style.fontSize = 12 * dpr + ‘px’; } else { doc.addEventListener(‘DOMContentLoaded’, function() { doc.body.style.fontSize = 12 * dpr + ‘px’; }, false); } refreshRem(); flexible.dpr = win.dpr = dpr; flexible.refreshRem = refreshRem; flexible.rem2px = function(d) { let val = parseFloat(d) * this.rem; if (typeof d === ‘string’ && d.match(/rem$/)) { val += ‘px’; } return val; }; flexible.px2rem = function(d) { let val = parseFloat(d) / this.rem; if (typeof d === ‘string’ && d.match(/px$/)) { val += ‘rem’; } return val; };});//})(window, window.lib || (window.lib = {})); ...

February 27, 2019 · 3 min · jiezi

WebPack4 学习笔记(1)

新建一个Demo文件夹,执行初始化:npm init在Demo文件夹里安装webpack和webpack-cli:npm install webpack webpack-cli -Dwebpack-cli可以使用命令行的方式来使用webpack,从版本4开始开始,webpack和webpack-cli分别管理,如果不安装webpack-cli, 就没法使用命令行了。安装完之后,执行webpack -v查看安装是否成功。命令执行后,会给你一个大大的错误提示,类似这样:webpack : 无法将“webpack”项识别为cmdlet、函数、脚本文件或可运行程序的名称。请检查名称的拼写,如果包括路径,请确保路 径正确,然后再试一次。这是因为webpack并非全局安装的,当输入这个命令时,npm会从全局的目录模块中找webpack,所以就找不到报错了。那怎么运行刚装好的webpack呢? 通过npx命令来运行就OK了:npx webpack -vnpx会寻找存在于项目内node_modules目录下的安装包。

February 27, 2019 · 1 min · jiezi

深入理解Webpack核心模块Tapable钩子[异步版]

接上一篇文章 深入理解Webpack核心模块WTApable钩子(同步版)tapable中三个注册方法1 tap(同步) 2 tapAsync(cb) 3 tapPromise(注册的是Promise)tapable中对三个触发方法1 call 2 callAsync 3 promise这一章节 我们将分别实现异步的Async版本和Promise版本异步钩子AsyncParallelHookAsyncParallelHook的Promise版本AsyncSeriesHookAsyncSeriesHook的Promise版本AsyncSeriesWaterfallHookAsyncSeriesWaterfallHook的Promise版本异步的钩子分为并行和串行的钩子,并行是指 等待所有并发的异步事件执行之后再执行最终的异步回调。而串行是值 第一步执行完毕再去执行第二步,以此类推,直到执行完所有回调再去执行最终的异步回调。AsyncParallelHookAsyncParallelHook是异步并行的钩子,上代码:const { AsyncParallelHook } = require(’tapable’);class Hook{ constructor(){ this.hooks = new AsyncParallelHook([’name’]); } tap(){ /** 异步的注册方法是tapAsync() * 并且有回调函数cb. / this.hooks.tapAsync(’node’,function(name,cb){ setTimeout(()=>{ console.log(’node’,name); cb(); },1000); }); this.hooks.tapAsync(‘react’,function(name,cb){ setTimeout(()=>{ console.log(‘react’,name); cb(); },1000); }); } start(){ /* 异步的触发方法是callAsync() * 多了一个最终的回调函数 fn. / this.hooks.callAsync(‘call end.’,function(){ console.log(‘最终的回调’); }); }}let h = new Hook();h.tap();/* 类似订阅 /h.start();/* 类似发布 // 打印顺序: node call end. react call end. 最终的回调*/等待1s后,分别执行了node call end和react callend 最后执行了最终的回调fn.手动实现:class AsyncParallelHook{ constructor(args){ /* args -> [’name’]) / this.tasks = []; } /* tap接收两个参数 name和fn / tap(name,fn){ /* 订阅:将fn放入到this.tasks中 / this.tasks.push(fn); } start(…args){ let index = 0; /* 通过pop()获取到最后一个参数 * finalCallBack() 最终的回调 / let finalCallBack = args.pop(); /* 箭头函数绑定this / let done = () => { /* 执行done() 每次index+1 / index++; if(index === this.tasks.length){ /* 执行最终的回调 / finalCallBack(); } } this.tasks.forEach((task)=>{ /* 执行每个task,传入我们给定的done回调函数 / task(…args,done); }); }}let h = new AsyncParallelHook([’name’]);/* 订阅 /h.tap(‘react’,(name,cb)=>{ setTimeout(()=>{ console.log(‘react’,name); cb(); },1000);});h.tap(’node’,(name,cb)=>{ setTimeout(()=>{ console.log(’node’,name); cb(); },1000);});/* 发布 /h.start(’end.’,function(){ console.log(‘最终的回调函数’);});/ 打印顺序: react end. node end. 最终的回调函数*/AsyncParallelHook的Promise版本const { AsyncParallelHook } = require(’tapable’);class Hook{ constructor(){ this.hooks = new AsyncParallelHook([’name’]); } tap(){ /** 这里是Promsie写法 * 注册事件的方法为tapPromise / this.hooks.tapPromise(’node’,function(name){ return new Promise((resolve,reject)=>{ setTimeout(()=>{ console.log(’node’,name); resolve(); },1000); }); }); this.hooks.tapPromise(‘react’,function(name){ return new Promise((resolve,reject)=>{ setTimeout(()=>{ console.log(‘react’,name); resolve(); },1000); }); }); } start(){ /* * promsie最终返回一个prosise 成功resolve时 * .then即为最终回调 / this.hooks.promise(‘call end.’).then(function(){ console.log(‘最终的回调’); }); }}let h = new Hook();h.tap();h.start();/ 打印顺序: node call end. react call end. 最终的回调*/这里钩子还是AsyncParallelHook钩子,只是写法变成了promise的写法,去掉了回调函数cb().变成了成功时去resolve().其实用Promise可以更好解决异步并行的问题,因为Promise的原型方法上有个all()方法,它的作用就是等待所有promise执行完毕后再去执行最终的promise。我们现在去实现它:class SyncHook{ constructor(args){ this.tasks = []; } tapPromise(name,fn){ this.tasks.push(fn); } promise(…args){ /** 利用map方法返回一个新数组的特性 / let tasks = this.tasks.map((task)=>{ /* 每一个task都是一个Promise / return task(…args); }); /* Promise.all() 等待所有Promise都执行完毕 / return Promise.all(tasks); }}let h = new SyncHook([’name’]);/* 订阅 /h.tapPromise(‘react’,(name)=>{ return new Promise((resolve,reject)=>{ setTimeout(()=>{ console.log(‘react’,name); resolve(); },1000); });});h.tapPromise(’node’,(name)=>{ return new Promise((resolve,reject)=>{ setTimeout(()=>{ console.log(’node’,name); resolve(); },1000); });});/* 发布 /h.promise(’end.’).then(function(){ console.log(‘最终的回调函数’);});/ 打印顺序: react end. node end. 最终的回调函数*/AsyncSeriesHookAsyncSeriesHook是异步串行的钩子, 串行,我们刚才说了, 它是一步步去执行的,下一步执行依赖上一步执行是否完成,手动实现:const { AsyncSeriesHook } = require(’tapable’);class Hook{ constructor(){ this.hooks = new AsyncSeriesHook([’name’]); } tap(){ /** 异步的注册方法是tapAsync() * 并且有回调函数cb. / this.hooks.tapAsync(’node’,function(name,cb){ setTimeout(()=>{ console.log(’node’,name); cb(); },1000); }); this.hooks.tapAsync(‘react’,function(name,cb){ /* 此回调要等待上一个回调执行完毕后才开始执行 / setTimeout(()=>{ console.log(‘react’,name); cb(); },1000); }); } start(){ /* 异步的触发方法是callAsync() * 多了一个最终的回调函数 fn. / this.hooks.callAsync(‘call end.’,function(){ console.log(‘最终的回调’); }); }}let h = new Hook();h.tap();h.start();/ 打印顺序: node call end. react call end. -> 1s后打印 最终的回调 -> 1s后打印*/AsyncParallelHook和AsyncSeriesHook的区别是AsyncSeriesHook是串行的异步钩子,也就是说它会等待上一步的执行 只有上一步执行完毕了 才会开始执行下一步。而AsyncParallelHook是并行异步 AsyncParallelHook 是同时并发执行。 ok.手动实现 AsyncSeriesHook:class AsyncParallelHook{ constructor(args){ /* args -> [’name’]) / this.tasks = []; } /* tap接收两个参数 name和fn / tap(name,fn){ /* 订阅:将fn放入到this.tasks中 / this.tasks.push(fn); } start(…args){ let index = 0; let finalCallBack = args.pop(); /* 递归执行next()方法 直到执行所有task * 最后执行最终的回调finalCallBack() / let next = () => { /* 直到执行完所有task后 * 再执行最终的回调 finalCallBack() / if(index === this.tasks.length){ return finalCallBack(); } /* index++ 执行每一个task 并传入递归函数next * 执行完每个task后继续递归执行下一个task * next === cb,next就是每一步的cb回调 / this.tasksindex++; } /* 执行next() / next(); }}let h = new AsyncParallelHook([’name’]);/* 订阅 /h.tap(‘react’,(name,cb)=>{ setTimeout(()=>{ console.log(‘react’,name); cb(); },1000);});h.tap(’node’,(name,cb)=>{ setTimeout(()=>{ console.log(’node’,name); cb(); },1000);});/* 发布 /h.start(’end.’,function(){ console.log(‘最终的回调函数’);});/ 打印顺序: react end. node end. -> 1s后打印 最终的回调函数 -> 1s后打印*/AsyncSeriesHook的Promise版本const { AsyncSeriesHook } = require(’tapable’);class Hook{ constructor(){ this.hooks = new AsyncSeriesHook([’name’]); } tap(){ /** 这里是Promsie写法 * 注册事件的方法为tapPromise / this.hooks.tapPromise(’node’,function(name){ return new Promise((resolve,reject)=>{ setTimeout(()=>{ console.log(’node’,name); resolve(); },1000); }); }); this.hooks.tapPromise(‘react’,function(name){ /* 等待上一步 执行完毕之后 再执行 / return new Promise((resolve,reject)=>{ setTimeout(()=>{ console.log(‘react’,name); resolve(); },1000); }); }); } start(){ /* * promsie最终返回一个prosise 成功resolve时 * .then即为最终回调 / this.hooks.promise(‘call end.’).then(function(){ console.log(‘最终的回调’); }); }}let h = new Hook();h.tap();h.start();/ 打印顺序: node call end. react call end. -> 1s后打印 最终的回调 -> 1s后打印*/手动实现AsyncSeriesHook的Promise版本class AsyncSeriesHook{ constructor(args){ this.tasks = []; } tapPromise(name,fn){ this.tasks.push(fn); } promise(…args){ /** 1 解构 拿到第一个first * first是一个promise / let [first, …others] = this.tasks; /* 4 利用reduce方法 累计执行 * 它最终返回的是一个Promsie / return others.reduce((l,n)=>{ /* 1 下一步的执行依赖上一步的then / return l.then(()=>{ /* 2 下一步执行依赖上一步结果 / return n(…args); }); },first(…args)); }}let h = new AsyncSeriesHook([’name’]);/* 订阅 /h.tapPromise(‘react’,(name)=>{ return new Promise((resolve,reject)=>{ setTimeout(()=>{ console.log(‘react’,name); resolve(); },1000); });});h.tapPromise(’node’,(name)=>{ return new Promise((resolve,reject)=>{ setTimeout(()=>{ console.log(’node’,name); resolve(); },1000); });});/* 发布 /h.promise(’end.’).then(function(){ console.log(‘最终的回调函数’);});/ 打印顺序: react end. node end. -> 1s后打印 最终的回调函数 -> 1s后打印*/最后一个AsyncSeriesWaterfallHook:AsyncSeriesWaterfallHookAsyncSeriesWaterfallHook 异步的串行的瀑布钩子,首先 它是一个异步串行的钩子,同时 它的下一步依赖上一步的结果返回:const { AsyncSeriesWaterfallHook } = require(’tapable’);class Hook{ constructor(){ this.hooks = new AsyncSeriesWaterfallHook([’name’]); } tap(){ this.hooks.tapAsync(’node’,function(name,cb){ setTimeout(()=>{ console.log(’node’,name); /** 第一次参数是err, 第二个参数是传递给下一步的参数 / cb(null,‘第一步返回第二步的结果’); },1000); }); this.hooks.tapAsync(‘react’,function(data,cb){ /* 此回调要等待上一个回调执行完毕后才开始执行 * 并且 data 是上一步return的结果. / setTimeout(()=>{ console.log(‘react’,data); cb(); },1000); }); } start(){ this.hooks.callAsync(‘call end.’,function(){ console.log(‘最终的回调’); }); }}let h = new Hook();h.tap();h.start();/ 打印顺序: node call end. react 第一步返回第二步的结果 最终的回调*/我们可以看到 第二步依赖了第一步返回的值, 并且它也是串行的钩子,实现它:class AsyncParallelHook{ constructor(args){ /* args -> [’name’]) / this.tasks = []; } /* tap接收两个参数 name和fn / tap(name,fn){ /* 订阅:将fn放入到this.tasks中 / this.tasks.push(fn); } start(…args){ let index = 0; /* 1 拿到最后的最终的回调 / let finalCallBack = args.pop(); let next = (err,data) => { /* 拿到每个task / let task = this.tasks[index]; /* 2 如果没传task 或者全部task都执行完毕 * return 直接执行最终的回调finalCallBack() / if(!task) return finalCallBack(); if(index === 0){ /* 3 执行第一个task * 并传递参数为原始参数args / task(…args, next); }else{ /* 4 执行处第二个外的每个task * 并传递的参数 data * data ->‘传递给下一步的结果’ / task(data, next); } index++; } /* 执行next() / next(); }}let h = new AsyncParallelHook([’name’]);/* 订阅 /h.tap(‘react’,(name,cb)=>{ setTimeout(()=>{ console.log(‘react’,name); cb(null,‘传递给下一步的结果’); },1000);});h.tap(’node’,(name,cb)=>{ setTimeout(()=>{ console.log(’node’,name); cb(); },1000);});/* 发布 /h.start(’end.’,function(){ console.log(‘最终的回调函数’);});/ 打印顺序: react end. node 传递给下一步的结果 最终的回调函数*/AsyncSeriesWaterfallHook的Promise版本const { AsyncSeriesWaterfallHook } = require(’tapable’);class Hook{ constructor(){ this.hooks = new AsyncSeriesWaterfallHook([’name’]); } tap(){ this.hooks.tapPromise(’node’,function(name){ return new Promise((resolve,reject)=>{ setTimeout(()=>{ console.log(’node’,name); /** 在resolve中把结果传给下一步 / resolve(‘返回给下一步的结果’); },1000); }); }); this.hooks.tapPromise(‘react’,function(name){ return new Promise((resolve,reject)=>{ setTimeout(()=>{ console.log(‘react’,name); resolve(); },1000); }); }); } start(){ this.hooks.promise(‘call end.’).then(function(){ console.log(‘最终的回调’); }); }}let h = new Hook();h.tap();h.start();/ 打印顺序: node call end. react 返回给下一步的结果 最终的回调*/用Promsie实现很简单,手动实现它吧:class AsyncSeriesHook{ constructor(args){ this.tasks = []; } tapPromise(name,fn){ this.tasks.push(fn); } promise(…args){ /** 1 解构 拿到第一个first * first是一个promise / let [first, …others] = this.tasks; /* 2 利用reduce方法 累计执行 * 它最终返回的是一个Promsie / return others.reduce((l,n)=>{ return l.then((data)=>{ /* 3 将data传给下一个task 即可 / return n(data); }); },first(…args)); }}let h = new AsyncSeriesHook([’name’]);/* 订阅 /h.tapPromise(‘react’,(name)=>{ return new Promise((resolve,reject)=>{ setTimeout(()=>{ console.log(‘react’,name); resolve(‘promise-传递给下一步的结果’); },1000); });});h.tapPromise(’node’,(name)=>{ return new Promise((resolve,reject)=>{ setTimeout(()=>{ console.log(’node’,name); resolve(); },1000); });});/* 发布 /h.promise(’end.’).then(function(){ console.log(‘最终的回调函数’);});/ 打印顺序: react end. node promise-传递给下一步的结果 最终的回调函数*/ok.至此,我们把tapable的钩子全部解析并手动实现完毕。写文章不易,喜欢的话给个赞或者start~代码在github上:mock-webpack-tapable ...

February 26, 2019 · 5 min · jiezi

进击webpack4 (优化篇)

进击webpack 4 (基础篇一)进击webpack4 (基础篇二:配置 一)进击webpack4 (基础篇三:配置 二)不解析不依赖第三方模块的模块noParse有一些第三方模块,它本身不依赖于其他模块,比如jquery,lodash,不去编译这些库,会使得webpack打包更加快速noParse是个正则或者包含正则的数组 RegExp | [RegExp]module:{ noParse:/jquery/, //不去解析jquery rules:[ //… ] },——————— 忽略某些库内的第三方模块ignorePlugin以moment这个时间库为例, 导入moment的同时, moment会引入自身依赖的语言包,这些语言包其中有部分是我们不需要用到的,moment内部代码 plugins: [ new webpack.IgnorePlugin(/^./locale$/, /moment$/) ],2个参数代表的意思是:当匹配到moment这个库的时候 引入moment并且忽略moment里面匹配到locale的库这个时候我们如果想要自己需要的locale 需在main.js手动引入import ‘moment/locale/zh-cn’动态链接库另起一个webpack.config.dll.js 专门用来生成动态链接库//webpack.config.dll.jsconst path=require(‘path’);const webpack=require(‘webpack’);module.exports={ mode:‘development’, entry: { react:[‘react’,‘react-dom’], jquery:[‘jquery’] },// 把 React 相关模块的放到一个单独的动态链接库 output: { path: path.resolve(__dirname,‘dist’),// 输出的文件都放到 dist 目录下 filename: ‘[name].dll.js’,//输出的动态链接库的文件名称,[name] 代表当前动态链接库的名称 library: ‘dll[name]’,//存放动态链接库的全局变量名称,例如对应 react 来说就是 _dll_react }, plugins: [ new webpack.DllPlugin({ // 动态链接库的全局变量名称,需要和 output.library 中保持一致 // 该字段的值也就是输出的 mainfest.json 文件 中 name 字段的值 // 例如 react.manifest.json 中就有 “name”: “_dll_react” name: ‘dll[name]’, // 描述动态链接库的 manifest.json 文件输出时的文件名称 path: path.join(__dirname, ‘dist’, ‘[name].mainfest.json’) }) ]}//打包npx webpack –config webpack.config.dll.js 这样会在dist生成然后在webpack.config.js里const webpack= require(‘webpack’)plugins: [ new webpack.DllReferencePlugin({ manifest:require(’./dist/react.mainfest.json’) }), new webpack.DllReferencePlugin({ manifest:require(’./dist/jquery.mainfest.json’) })]这里它会从mainfest.json寻找name 然后根据它的标识找到相应内容, dll.js就是打包出来后的动态链接库然后在html模板文件里引入<script src="./jquery.dll.js"></script><script src="./react.dll.js"></script>如果你在main.jsimport React from ‘react’,他会首先找动态链接库, 找不到才会执行打包注:使用react需要配置好rule{test:/.js/, use:{ loader:‘babel-loader’, options:{ presets:[ ‘@babel/preset-env’, ‘@babel/preset-react’ ] } }},开启多进程打包npm i happypack -D如果一个项目代码密集,读写操作频繁,happypack 就能让Webpack把任务分解给多个子进程去并发的执行,子进程处理完后再把结果发送给主进程。const HappyPack = require(‘happypack’); rules: [ { test: /.js$/, // 把对 .js 文件的处理转交给 id 为 babel 的 HappyPack 实例 use: [‘happypack/loader?id=babel’], exclude: path.resolve(__dirname, ’node_modules’), }, { test: /.css$/, // 把对 .css 文件的处理转交给 id 为 css 的 HappyPack 实例 use: [‘happypack/loader?id=css’] }]new Happypack({ //ID是标识符的意思,ID用来代理当前的happypack是用来处理一类特定的文件的 id: ‘js’, use: [{ loader: ‘babel-loader’, //options=query都是向插件传递参数的 options: { presets: [["@babel/preset-env", { modules: false }], “@babel/preset-react”], plugins: [ ["@babel/plugin-proposal-decorators", { “legacy”: true }], ["@babel/plugin-proposal-class-properties", { “loose”: true }], ] } }] }), new Happypack({ //ID是标识符的意思,ID用来代理当前的happypack是用来处理一类特定的文件的 id: ‘css’, use: [MiniCssExtractPlugin.loader, ‘css-loader’, ‘postcss-loader’], threads: 4,//你要开启多少个子进程去处理这一类型的文件 verbose: true//是否要输出详细的日志 verbose })注意:开启进程也需要时间, 如果一个项目并不是很复杂, 斟酌使用不打包第三方库 使用cdn引入module.exports = { //… externals: { jquery: ‘jQuery’ }};main.js 引入jquery 将不会打包 import jquery from ‘jQuery’html模板内引入jquery的cdn地址即可尽量使用es6的模块导入webpack的tree-shaking自己可以分析出哪些没有使用到的代码可以剔除,前提是es6模块语法scope-hosting可以提升作用域 比如 var a = 1; var b = a ; console.log(b) 会编译成var b = 1; console.log(b)提取公共代码做这种操作首先得是多页面 entry:{ home:[’./src/index.js’], login:[’./src/login.js’]}, // 入口文件//home.jsimport React from ‘react’import {render }from ‘react-dom’render(<h1>动态链接库</h1>,window.root)//login.jsimport React from ‘react’import {render }from ‘react-dom’render(<h1>动态链接库</h1>,window.root)//webpack.config.jsoptimization:{ // 优化 splitChunks:{ //分割代码 cacheGroups:{ // 缓存组 common:{ // 公共的代码 一般是自己写的公共代码 chunks:‘initial’, minSize:0, minChunks: 2, //最少被引用2次的模块 name: “common” }, vendor:{ // 一般是第三方公共模块 priority:1, // 因为执行是从上往下, 所以设置优先级比上面高 不然上面抽离的话第三方模块也被抽离了 test:/node_modules/ , //匹配node_modules下的公共代码, chunks:‘initial’, minSize:0, minChunks: 2, //最少被引用2次的模块 name: “vendor” } } }}懒加载这里拿vue举例const Login = () => import(/* webpackChunkName: “login” */,"./login");new VueRouter({ routes: [{ path: “/login”, component: Login }]})webpackChunkName虽然是注释, 但是webpack能识别, 编译后这个组件生产的名字就是login可能会需要@babel/plugin-syntax-dynamic-import 才能识别yarn add @babel/plugin-syntax-dynamic-import -D具体配置看此文热更新devServer:{ // 告诉 DevServer 要开启模块热替换模式 hot: true, } 在vue中只要这样配置就可以了, vue自己帮我们做了配置其他库中:import React from ‘react’import {render} from ‘react-dom’render(<App/>, document.getElementById(‘root’));if (module.hot) { // accept 函数的第一个参数指出当前文件接受哪些子模块的替换,这里表示只接受 ./AppComponent 这个子模块 // 第2个参数用于在新的子模块加载完毕后需要执行的逻辑 module.hot.accept([’./App’], () => { // 新的 AppComponent 加载成功后重新执行下组建渲染逻辑 let App=require(’./App’).default; render(<App/>, document.getElementById(‘root’)); });} ...

February 26, 2019 · 2 min · jiezi

深入理解Webpack核心模块Tapable钩子[同步版]

记录下自己在前端路上爬坑的经历 加深印象,正文开始~tapable是webpack的核心依赖库 想要读懂webpack源码 就必须首先熟悉tapableok.下面是webapck中引入的tapable钩子 由此可见 在webpack中tapable的重要性const { SyncHook, SyncBailHook, SyncWaterfallHook, SyncLoopHook, AsyncParallelHook, AsyncParallelBailHook, AsyncSeriesHook, AsyncSeriesBailHook, AsyncSeriesWaterfallHook } = require(“tapable”);这些钩子可分为同步的钩子和异步的钩子,Sync开头的都是同步的钩子,Async开头的都是异步的钩子。而异步的钩子又可分为并行和串行的钩子,其实同步的钩子也可以理解为串行的钩子。本文将以以下章节分别梳理每个钩子同步钩子SyncHookSyncBailHookSyncWaterfallHookSyncLoopHook异步钩子AsyncParallelHookAsyncParallelHook的Promise版本AsyncSeriesHookAsyncSeriesHook的Promise版本AsyncSeriesWaterfallHookAsyncSeriesWaterfallHook的Promise版本首先安装tapable npm i tapable -DSyncHookSyncHook是简单的同步钩子,它很类似于发布订阅。首先订阅事件,触发时按照顺序依次执行,所以说同步的钩子都是串行的。const { SyncHook } = require(’tapable’);class Hook{ constructor(){ /** 1 生成SyncHook实例 / this.hooks = new SyncHook([’name’]); } tap(){ /* 2 注册监听函数 / this.hooks.tap(’node’,function(name){ console.log(’node’,name); }); this.hooks.tap(‘react’,function(name){ console.log(‘react’,name); }); } start(){ /* 3出发监听函数 / this.hooks.call(‘call end.’); }}let h = new Hook();h.tap();/* 类似订阅 / h.start();/* 类似发布 // 打印顺序: node call end. react call end./可以看到 它是按照顺序依次打印的,其实说白了就是发布和订阅。接下来我们就手动实现它。class SyncHook{ // 定义一个SyncHook类 constructor(args){ / args -> [’name’]) / this.tasks = []; } /* tap接收两个参数 name和fn / tap(name,fn){ /* 订阅:将fn放入到this.tasks中 / this.tasks.push(fn); } start(…args){/* 接受参数 / /* 发布:将this.taks中的fn依次执行 / this.tasks.forEach((task)=>{ task(…args); }); }}let h = new SyncHook([’name’]);/* 订阅 /h.tap(‘react’,(name)=>{ console.log(‘react’,name);});h.tap(’node’,(name)=>{ console.log(’node’,name);});/* 发布 /h.start(’end.’);/ 打印顺序: react end. node end./SyncBailHookSyncBailHook 从字面意思上理解为带有保险的同步的钩子,带有保险意思是 根据每一步返回的值来决定要不要继续往下走,如果return了一个非undefined的值 那就不会往下走,注意 如果什么都不return 也相当于return了一个undefined。const { SyncBailHook } = require(’tapable’);class Hook{ constructor(){ this.hooks = new SyncBailHook([’name’]); } tap(){ this.hooks.tap(’node’,function(name){ console.log(’node’,name); /** 此处return了一个非undefined * 代码到这里就不会继续执行余下的钩子 / return 1; }); this.hooks.tap(‘react’,function(name){ console.log(‘react’,name); }); } start(){ this.hooks.call(‘call end.’); }}let h = new Hook();h.tap();h.start();/ 打印顺序: node call end./手动实现class SyncHook{ constructor(args){ this.tasks = []; } tap(name,fn){ this.tasks.push(fn); } start(…args){ let index = 0; let result; /** 利用do while先执行一次的特性 / do{ /* 拿到每一次函数的返回值 result / result = this.tasksindex++; /* 如果返回值不为undefined或者执行完毕所有task -> 中断循环 / }while(result === undefined && index < this.tasks.length); }}let h = new SyncHook([’name’]);h.tap(‘react’,(name)=>{ console.log(‘react’,name); return 1;});h.tap(’node’,(name)=>{ console.log(’node’,name);});h.start(’end.’);/ 打印顺序: react end./SyncWaterfallHookSyncWaterfallHook是同步的瀑布钩子,瀑布怎么理解呢? 其实就是说它的每一步都依赖上一步的执行结果,也就是上一步return的值就是下一步的参数。const { SyncWaterfallHook } = require(’tapable’);class Hook{ constructor(){ this.hooks = new SyncWaterfallHook([’name’]); } tap(){ this.hooks.tap(’node’,function(name){ console.log(’node’,name); /** 此处返回的值作为第二步的结果 / return ‘第一步返回的结果’; }); this.hooks.tap(‘react’,function(data){ /* 此处data就是上一步return的值 / console.log(‘react’,data); }); } start(){ this.hooks.call(‘callend.’); }}let h = new Hook();h.tap();h.start();/ 打印顺序: node callend. react 第一步返回的结果/手动实现:class SyncWaterFallHook{ constructor(args){ this.tasks = []; } tap(name,fn){ this.tasks.push(fn); } start(…args){ /** 解构 拿到tasks中的第一个task -> first / let [first, …others] = this.tasks; /* 利用reduce() 累计执行 * 首先传入第一个 first 并执行 * l是上一个task n是当前task * 这样满足了 下一个函数依赖上一个函数的执行结果 / others.reduce((l,n)=>{ return n(l); },first(…args)); }}let h = new SyncWaterFallHook([’name’]);/* 订阅 /h.tap(‘react’,(name)=>{ console.log(‘react’,name); return ‘我是第一步返回的值’;});h.tap(’node’,(name)=>{ console.log(’node’,name);});/* 发布 /h.start(’end.’);/ 打印顺序: react end. node 我是第一步返回的值*/SyncLoopHookSyncLoopHook是同步的循环钩子。 循环钩子很好理解,就是在满足一定条件时 循环执行某个函数:const { SyncLoopHook } = require(’tapable’);class Hook{ constructor(){ /** 定义一个index / this.index = 0; this.hooks = new SyncLoopHook([’name’]); } tap(){ /* 箭头函数 绑定this / this.hooks.tap(’node’,(name) => { console.log(’node’,name); /* 当不满足条件时 会循环执行该函数 * 返回值为udefined时 终止该循环执行 / return ++this.index === 5?undefined:‘学完5遍node后再学react’; }); this.hooks.tap(‘react’,(data) => { console.log(‘react’,data); }); } start(){ this.hooks.call(‘callend.’); }}let h = new Hook();h.tap();h.start();/ 打印顺序: node callend. node callend. node callend. node callend. node callend. react callend./可以看到 执行了5遍node callend后再继续往下执行。也就是当返回undefined时 才会继续往下执行:class SyncWaterFallHook{ constructor(args){ this.tasks = []; } tap(name,fn){ this.tasks.push(fn); } start(…args){ let result; this.tasks.forEach((task)=>{ /** 注意 此处do{}while()循环的是每个单独的task / do{ /* 拿到每个task执行后返回的结果 / result = task(…args); /* 返回结果不是udefined时 会继续循环执行该task / }while(result !== undefined); }); }}let h = new SyncWaterFallHook([’name’]);let total = 0;/* 订阅 /h.tap(‘react’,(name)=>{ console.log(‘react’,name); return ++total === 3?undefined:‘继续执行’;});h.tap(’node’,(name)=>{ console.log(’node’,name);});/* 发布 /h.start(’end.’);/ 打印顺序: react end. react end. react end. node end./可以看到, 执行了3次react end.后 才继续执行了下一个task -> node end。至此,我们把tapable的所有同步钩子都解析完毕. 异步钩子比同步钩子麻烦些,我们会在下一章节开始解析异步的钩子. ...

February 26, 2019 · 3 min · jiezi

【手牵手】搭建前端组件库(一)

手牵手搭建前端组件库本文梳理如何搭建和构建前端组件库.了解几个问题为何需要组件化?大部分项目起源都是源于业务方的各种各样的奇葩需求。随着公司的业务发展,公司内部开始衍生出很多的B2C系统、后台系统,前端部门也疲于应对越来越多同质化的项目,这些项目在很多基础模块层、源代码存在不小的相似,甚至存在相似的业务模块。笔者曾经所在的一个电商团队,前端成员基本每个人多做过登录注册、购物车、支付、微信登录…… 大量重复的业务代码。由于组内技术没有强制规范本质上相同的东西,重复的去code就显得浪费.分析这些问题发现:日渐增多的业务场景需求前端资源有限,无法支持所有项目的快速迭代公司内部诸多产品业务混乱、体验不统一于是开发底层的工具去服务不同业务就很有必要:设计一套公司内部的基础组件库支撑各个前端项目,提升项目和业务的可用性和一致性。一个前端团队拥有大量的业务场景和业务代码,相似的页面和代码层出不穷,如何管理和抽象这些相似的代码和模块,绝大多数团队会遇到这样的问题。 不断的拷代码? 修改代码?还是抽象成组件?显然后者更高效。所以在多项目存在高度的可控、底层依赖的情况下,前端实现组件库是最好的选择。组件化,又或者组件抽离的目的是为了功能共享方便维护,其能够带来的好处是少写代码,统一管理、统一维护。一套基础组件代码千锤百炼精而又精,从而起到快速支撑业务迭代,提升开发效率的目的。业务型组件库前端组件库百花齐放,antd、element ui这些基础组件库已经很强大,使用于各种业务场景。但是这些基础组件的粒度是基于单个交互,而在交互与产品之间隔着各种各样的模块和业务场景,产品的汇聚源于各种基础组件在业务逻辑的沾粘下集成为一个个项目,一个团队或多或少会有项目或模块存在功能、交互流程的重复、本质上的同质化。所以antd、element ui 这类组件库是基于单个非连续性的交互组件,一个组件代表着一次人机无副作用的操作与响应,其不思考实体、用户、终端的状态,最小化的暴露和响应组件内部状态。对于连续性的交互通常来说与特点的业务场景有关,存在诸多的外部依赖,目前都是在各个业务模块由用户(coder)自行编写。有没有一种方法解决连续性交互流程的共用问题?解决的办法是组件封装包含业务场景的连续性交互流程,利用组件化将内部依赖通过接口映射到外部。前端架构部门为业务部门提供业务型组件库能够有效提高开发效率.组件库设计思路组件是对一些具有相同业务场景和交互模式、交互流程代码的抽象,组件库首先应该保证各个组件的视觉风格和交互规范保持一致。组件库的 props 定义需要具备足够的可扩展性,对外提供组件内部的控制权,使组件内部完全受控。支持通过 children 自定义内部结构,预定义组件交互状态。保持组件具有统一的输入和输出,完整的API.组件库的开发我们需要考虑:组件设计思路、需要解决的场景组件代码规范组件测试组件维护,包括迭代、issue、文档、发布机制一个完整强大的组件库需要多方面努力,回归正题.使用到的基础技术vue cil 3npmwebpackrollup(v1.2.2)Demo下面就手把手搭建一个前端偏业务性的组件库。组件库包括:message 组件: 一个封装用于呈现后台通过 websocket 推送到前台页面的实时消息模块;pay 组件: 一个封装用于实现商品支付的模块share 组件: 一个封装用于实现商品、文章、视频在各社交平台分享的模块只抛出一个栗子,组件内部实现略这里注意组件抽取的粒度,组件的抽离以一个完整的连续性交互为目地。组件依赖数据、交互事件、控制权的暴露需要考虑全面,不同的上层业务部门都有自己对组件可配置的不同渴望。需要权衡,不能把配置化给捣鼓的永无止境到很难堪的局面。笔者曾经就参与一个项目的组件化,组件抽离的面目全非,各种依赖、环境、状态的配置,导致最后只有组件编写人员在看文档加回忆的情况下才能搞清楚其来龙去脉.从简单的开始1、初始化组件库目录创建一个空项目// 新建一个项目vue create qw-ui经过vue cil3初始化后的qw-ui目录:├─docs│ ├─public│├─src│ .gitignore│ babel.config.js│ package-lock.json│ package.json│ README.md│ vue.config.js│ 此时为了方便组件库的代码管理,将目录结构修改为:├─src // 用作示例 Demo│ ├─packages // 新增 packages 用于编写存放组件│├─lib // 新增 lib 用于存放编译后的输出文件│ .gitignore│ babel.config.js│ package-lock.json│ package.json│ README.md│ vue.config.js│ 目录结构可以更具需要调整.2、修改 vue.config.js 配置vue cli3 提供一个可选的 vue.config.js 配置文件。这个文件存在则他会被自动加载,所有的对项目和webpack的配置,都在这个文件中。修改 vue.config.js 配置的目的主要是:使 Demo 可访问,实现对 src目录的编译处理;提供对 package的编译、构建处理做以下两处修改:修改项目的入口entry 字段为项目入口入口修改使用 Vue CLI 3 的 page属性来配置:module.exports = { pages: { index: { // page 的入口 entry: ‘src/main.js’, // 模板来源 template: ‘public/index.html’, // 在 dist/index.html 的输出 filename: ‘index.html’ } }}添加对 packages 目录的编译处理packages 是我们后来新增的一个目录,默认是不被 webpack 处理的,所以需要通过添加配置对该目录的编译支持。新增编译处理目录,需要通过webpack的链式操作chainWebpack函数实现:module.exports = { pages: { index: { // page 的入口 entry: ’examples/main.js’, // 模板来源 template: ‘public/index.html’, // 在 dist/index.html 的输出 filename: ‘index.html’ } }, chainWebpack: config => { // packages和examples目录需要加入编译 config.module .rule(‘js’) .include.add(/packages/) .end() .include.add(/src/) .end() .use(‘babel’) .loader(‘babel-loader’) .tap(options => { // 修改它的选项… return options; }); }}执行 npm run vue-cli-service serve , 实现对Demo的访问.3、编写 packages 组件库创建一个 message组件创建组件在 packages 目录下,所有的单个组件都以文件夹的形式存储,这里创建一个目录 message 文件夹;在 message/ 目录下创建 src/ 目录存储组件源码,所有 message 依赖的除第三方资源都存放与该目录下;在 /message目录下创建 index.js` 文件对外提供对组件的引用示例代码:message/index.js 对外提供应用// message/index.jsimport message from ‘./src/message ‘message .install = function (Vue) { Vue.component(message .name, message )}export default message // message/src/message .js<template> <div class=“message”> <el-row class=“message-test”> <el-col :span=“12” class=“message-row”><p class=“text”>hello {{ message }}</p></el-col> <el-col :span=“6”> <img src="./st.png"/> </el-col> </el-row> </div></template><script>import ‘./index.scss’export default { name: ‘v-message’, // 申明组件的 name属性 props: { message: String }}</script>需要注意的是,组件 mesage 必须声明 name 属性,这个 name 就是组件的标签,如:<v-message><v-message/>packages/message目录结构如下:packages/message ├─index.js │ ├─src │ message.vue │ st.png // 组件依赖的图片 │ index.scss // 组件依赖的样式文件导出 packages 组件库修改 /packages/index.js 文件,整合所有组件,并对整个组件库进行导出:// 导入组件import hello from ‘./hello’// 存储组件列表const components = [ hello]// 定义 install 方法,接收 Vue 作为参数。如果使用 use 注册插件,则所有的组件都将被注册const install = function (Vue) { // 判断是否安装 if (install.installed) return // 遍历注册全局组件 components.map(component => Vue.component(component.name, component))}// 判断是否是直接引入文件if (typeof window !== ‘undefined’ && window.Vue) { install(window.Vue)}export default { // 导出的对象必须具有 install,才能被 Vue.use() 方法安装 install, // 以下是具体的组件列表 hello}到此,构建组件库的环境准好好了### 4、发布组件库到 npmpackages 目录的编译打包在 package.json的 scripts 字段中新增一下命令:“lib”: “vue-cli-service build –target lib –name kui –dest lib packages/index.js"vue cil3 提供了 库模式 来打包第三方库的开发,packages 的编译打包需要使用库模式–target: 构建目标,默认为应用模式。这里修改为 lib 启用库模式。–dest : 输出目录,默认 dist。这里我们改成 lib[entry]: 最后一个参数为入口文件,默认为 src/App.vue。这里我们指定编译 packages/ 组件库目录。在 vue cil3 库模式中,Vue 是外置的。这意味着包中不会有 Vue,即便你在代码中导入了 Vue。如果这个库会通过一个打包器使用,它将尝试通过打包器以依赖的方式加载 Vue;否则就会回退到一个全局的 Vue 变量。配置好了后,执行编译命令:npm run lib稍后控制台输出,即编译完成: DONE Compiled successfully in 5988ms16:05:35 File Size Gzipped lib\kui.umd.min.js 8.08 KiB 4.55 KiB lib\kui.umd.js 17.78 KiB 7.31 KiB lib\kui.common.js 17.41 KiB 7.19 KiB lib\kui.css 0.10 KiB 0.10 KiB Images and other types of assets omitted. Total task duration: 8.71s ```package.json 配置name: 包名,该名字是唯一的。可在 npm 官网搜索名字。version: 版本号,每次发布至 npm 需要修改版本号,不能和历史版本号相同。description: 描述。main: 入口文件,该字段需指向我们最终编译后的包文件。keyword:关键字,以空格分离希望用户最终搜索的词。author:作者private:是否私有,需要修改为 false 才能发布到 npmlicense: 开源协议参考配置:{ “name”: “qw-ui”, “version”: “0.1.0”, “private”: false, “main”: “lib/kui.umd.min.js”, “description”: “qw-ui”, “keyword”: “qw-ui”, “author”:“luojh”, “scripts”: { “serve”: “vue-cli-service serve”, “build”: “vue-cli-service build”, “lint”: “vue-cli-service lint”, “lib”: “vue-cli-service build –target lib –name kui –dest lib packages/index.js” }}添加 .npmignore 文件发布时,只有编译后的 lib 目录、package.json、README.md才需要被发布。所以通过配置.npmignore文件忽略不需要提交的目录和文件。# 忽略目录examples/packages/public/# 忽略指定文件vue.config.jsbabel.config.js*.map# 本地文件.env.local.env..local# 日志文件npm-debug.logyarn-debug.logyarn-error.log# 编辑器缓存文件.idea.vscode*.suo*.ntvs**.njsproj*.sln*.sw*发布到 npm首先需要在 npm 官网上注册一个账号,通过 npm adduser 命令创建一个账户,或者在 npm 官网注册注册完成后在本地命令行中登录:npm login执行发布命令,发布到 npmnpm publishnpm 淘宝镜像不支持 publish 命令,如果设置了淘宝镜像,publish 前需将镜像设置会 npm :npm config set registry http://registry.npmjs.orgnpm publish时,本地cmd终端需通过管理员运行### 5.使用组件库安装发布的组件库:npm i qw-ui使用组件:# 在 main.js 引入并注册import qwui from ‘qw-ui’Vue.use(qwui)# 在组件中使用<template> <v-message message=” hello 333 :: 使用kui组件库"></v-message></template><script> export default { data () { return { } } }</script>完! ...

February 26, 2019 · 3 min · jiezi

【手牵手】搭建前端组件库(二)

进阶组件库按需引入在目前,所有的组件会被打包进一个文件,组件库是一骨碌加载完所有组件,同时也会打包和加载多余的代码。对于小项目这样没有问题,但是当组件库越来越庞大、丰富,特别是像我们带业务逻辑的非js库,代码量会更大,如果不管不顾的一通加载完所有资源,后期肯定会带来业务方面的体验问题。所以首要的问题是实现源代码的按需引入,而按需引入的前提是实现源码包按独立组件分割和的拆分打包。代码分拆单个组件独立构建打包的思路就是给打包工具提供多个独立的入口,根据入口收集其所依赖的资源。一个组件入口产出一个文件webpack使用 webpack 配置多入口的方式来按模块拆分打包,每一个模块作为一个入口,与此同时产出对应的文件。webpack 的配置比较简单,不展开.实际构建 library 使用 webpack 有不小的缺点, 应为 webpack 产出后的文件中带有一层包裹代码,这种情况下在碎片化的组件库中反而会使打包体积变大,无法起到‘瘦身’的效果。如下的 webpack 包裹代码:/* 1 //*/ (function(module, webpack_exports, webpack_require) { ‘use strict’; / unused harmony export square / / harmony export (immutable) */ webpack_exports[‘a’] = cube; function square(x) { return x * x; } function cube(x) { return x * x * x; }});哪些额外的代码看着有点不那么清爽.rollup使用 rollup来打包 library,rollup相较于 webpack 在打包体积上会跟小,更加适合 .rollup 的特点:Tree Shaking: 自动移除未使用的代码, 输出更小的文件Scope Hoisting: 所有模块构建在一个函数内, 执行效率更高Config 文件支持通过 ESM 模块格式书写可以一次输出多种格式:模块规范: IIFE, AMD, CJS, UMD, ESMDevelopment 与 production 版本: .js, .min.js是驴是马拉出来溜溜全局安装 rollupnpm install –global rollup// orcnpm install –global rolluprollup 的迭代比较快,这里稍微留意一下 rollup 的版本,部分插件可能不兼容添加rollup.config.js在项目根目录下创建 rollup.config.js 文件:// rollup.config.jsexport default { input: ‘packages/index.js’, output: { file: ’lib/app.all.js’, format: ‘cjs’ }};input:构建入口format:编译打包格式file:编译后输出目录就这么简单,执行以下命令开始将装个 packages 构建构建为一个单文件rollup -c rollup.config.js添加 rollup 多文件构建Rollup 配置文件是一个标准 ES6 模块,默认到处一个对象,也可以到处一个对象用来构建多个模块// rollup.config.jsexport default [{ input: ‘packages/a.js’, output: { file: ’lib/app.a.js’, format: ‘cjs’ }},{ input: ‘packages/b.js’, output: { file: ’lib/app.b.js’, format: ‘cjs’ }}];packages 目录为组件库源码,相关模块不固定,不适宜写死。对于这个问题通过扫描目录获取所有模块,修改 rollup.config.js :// rollup.config.jsconst fs = require(‘fs-extra’);const path = require(‘path’);const packages = {};const dir = path.join(__dirname, ‘../packages’);const files = fs.readdirSync(dir);files.forEach(file => { const absolutePath = path.join(dir, file); if (isDir(absolutePath)) { packages[file] =packages/${file}/index.js; }});function createRollupConfig (file, name) { const config = { input: file, output: { file: lib/index.js : lib/${name}/index.js, format: ’es’, name: name, sourcemap: true } } return config}const buildPackages = []for (let name in packages) { const file = packages[name] buildPackages.push(createRollupConfig(file, name))}export default buildPackages;这里打包文件的格式我们使用 es,es是指ES6.这个时候开始构建会报错,因为rollup还不能识别组件库中的 vue 样板代码、语法,同时我们的组件库并不是纯粹的js library, 也需要处理业务组件内引用的样式和图片、字体等。仅仅是使用 rollup 还不能实现我们的目的,需要借助一系列 rollup 插件来完成处理vue.vue文件的编译需要使用rollup-plugin-vue2插件:npm rollup-plugin-vue2 –save-dev处理样式样式处理可以使用 rollup-plugin-css-only插件。由于不喜欢笨拙的css,习惯了scss语法,就直接使用 scss,但其配置就相对要复杂一点。scss样式处理可以使用rollup-plugin-scss插件,为了灵活的处理样式我使用Postcss图片处理html中引入的图片在组件库部署后就无法正常访问了,这里使用 rollup-plugin-url插件将内嵌的图片编译为 base64 再直接放入 js 文件中。对于组件库中有较多大尺寸的图片建议直接将图片放入图片服务器,然后通过url 引入,避免打包文件过大的问题.加入 rollup 插件后的配置:// rollup.config.jsconst fs = require(‘fs-extra’);const path = require(‘path’);import vue from ‘rollup-plugin-vue2’import postcss from ‘rollup-plugin-postcss’import postcssScss from ‘postcss-scss’import autoprefixer from ‘autoprefixer’import base64 from ‘postcss-base64’import url from ‘rollup-plugin-url’import progress from ‘rollup-plugin-progress’import filesize from ‘rollup-plugin-filesize’;function isDir(dir) { return fs.lstatSync(dir).isDirectory();}const packages = {};const dir = path.join(__dirname, ‘../packages’);const files = fs.readdirSync(dir);files.forEach(file => { const absolutePath = path.join(dir, file); if (isDir(absolutePath)) { packages[file] =packages/${file}/index.js; }});function createRollupConfig (file, name) { const config = { input: file, output: { file: lib/${name}/index.js, format: ’es’, name: name, sourcemap: true }, plugins: [ vue(), postcss({ extract: true, parser: postcssScss, plugins: [ base64({ extensions: [’.png’, ‘.jpeg’], root: ‘./packages/’, }), autoprefixer({ add: true }), ] }), url({ limit: 10 * 1024, //include: [’.svg’] }), progress(), filesize() ] } return config}const buildPackages = []for (let name in packages) { const file = packages[name] buildPackages.push(createRollupConfig(file, name))}export default buildPackages;到此可以运行 rollup -c rollup.config.js 打包,实现源代码按依赖关系和目录进行分拆打包:lib ├─message │ index.js | index.css | index.js.map ├─pay │ index.js | index.css | index.js.map ├─share │ index.js | index.css | index.js.map打包后的 packages/pay/index.js > lib/pay/index.js :// lib/pay/index.jsvar logo = “data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAADICAYAAACtWK6eAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAyNpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMDE0IDc5LjE1Njc5NywgMjAxNC8wOC8yMC0wOTo1MzowMiAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6OTk2QkI4RkE3NjE2MTFFNUE4NEU4RkIxNjQ5MTYyRDgiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6OTk2QkI4Rjk3NjE2MTFFNUE4NEU4RkIxNjQ5MTYyRDgiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENDIChNYWNpbnRvc2gpIj4gPHhtcE1NOkRlcml2ZWRGcm9tIHN0UmVmOmluc3RhbmNlSUQ9InhtcC5paWQ6NjU2QTEyNzk3NjkyMTFFMzkxODk4RDkwQkY4Q0U0NzYiIHN0UmVmOmRvY3VtZW50SUQ9InhtcC5kaWQ6NjU2QTEyN0E3NjkyMTFFMzkxODk4RDkwQkY4Q0U0NzYiLz4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz5WHowqAAAXNElEQVR42uxda4xd1XVe53XvvD2eGQ/lXQcKuDwc2eFlCAGnUn7kT6T86J/+aNTgsWPchJJYciEOCQ8hF+G0hFCIHRSEqAuJBCqRaUEIEbmBppAIBGnESwZje8COZ+y587j3PLq+ffadGJix53HvPevcuz60xPjec89ZZ+39nf04+9vLSZKEFArFzHA1BAqFEkShUIIoFEoQhUIJolAoQRQKJYhCoQRRKJQgCoUSRKFQKEEUCiWIQrFo+Gv/8/YH+f/nsMWSHHMChyhxqPTTdyncWyJ3ScD/ztipiB3wXSqu6P17avN+TyFC5ggv4tRnmoxWTP1+5F+Mz17GPvPl49EKBWd3UsfXllPiso8VcYtmPba3fNuKrBVXrGFCbrdPwXndFL49ltI367roOpSUI4pGypv9s7q+ltj6JxqOQ07Bo/DgxGb2/a8cX0CnAWXJ5etz2TqdHiXHKlKj9w6i9XX8Ic41DmI8FVHhmmXk85MmRhCzJoiTWnig9LfJRHihgydxzAxJhBr7Bh/hK3yu+p9568FliTJF2aKMZfVd/kQOcKP6OBmS9+Rjm4zJ6faoeN0gOUn61MncLX4CJ+MRhe+P/dRxhfew2Df4CF/hs4jWg8vQYUKYMuWyRRkLjeHQ8YP0Z9mekVjA8Qj3VVcuoeDiXu63lkUE0ym6FA5PXBaNVr7qtPumGyPR4Bt8hK/wWUR5chn6XJYoU5StUHL8l+XEx2axhkS6yk+chJuP4rXLyOkIKJkS0B67adcqfL/0Y4pixxSysK6V8Yl9Mz7i3272NRFlhzJsu24Z5l9E9Ahmwfrpoj7uw3fZtktsRZKjIXnndlLxin7+W8ZTBwPf6I+Tg9HwxK2Ob8citbCoBoaxBxMCvsFH+CqjHCtUvLzflKWUcpwB91gupG5f9/Rtx39ZZBtmWyJtphKzHTQW0diP36b4aJmcLj/zGaSkHJPb4SWFi/tOJd8bTqd9s48VBRh4RKeUX/vjgXg8cpyCmz05xkJylxSoa8M5RF0eJaVIIkGOsg2yTc3UgpD94psiWxEOqDNYoOIXuHnGwE5AXUTFi46FTnRw4l/dwEm7/pSxcYnCF/gE3zInh52RRJkVP7/MlKFQcgCbjifHTAQBfsb2qsgBO3e1Cpf3UXBej3nRJKKrxU/rcH/pKzz4vNIQuRJTEmZklbg6EL4SPsE3GQPzinmfhbJDGQolB+r8w58abs5y8DqRt4ABeptLRR7koY9NleybEYw/MPisvF/ayT1/SvDewcnIcG32wfiCAbEvoCZyGaGsitdyz6XdTctQJq6fcT5mloNfYvu5yFZkpEz+RT0UrFoqpxVBV+vQxIrkaPnrbqdvXs6hcjbU+Jq4Nvvwd/BFRNeq2npwWfkX95iyE9p6PM72P/MhCPANTBSKu5WITHcC074Y9CUTkYglKBgcV/aVtlM5Kpp/RHFjDdfka7MP/2wG6m72661QNigjlBXKTGBtsjWKNs5atCf44Uds3xc5YD8Wknd2BxWuGjCzIxLWQzlFj+IjU108OL7bafM5sm5DDdfka/8T+9AJXyTMpqFsUEYoK5SZ0NbjVlvX500Q4Ha2A+JuCcEvhVS8qp/8MzspHhMSfO7mVPaP35BMRp9JsCQldbX+hmvxNfnamzJfqVvtWnGZoGxQRigroYs6UbfvOGHn4ORVkTaIbEWwtqg3MNO+Zql0JGCdVuCayhDuG9uJB7vp+oR17FbZc+NauCauLWLmKkqXr6NsUEYoK6GtxwY6CXXnEs0n2faIHLCPhhR8bikFKwRN+xZddHWu5a7Ol9yCZ2ZwHKdOxufGNeKRqS/hmnLWW1VMmQSrl5oyEkqOPbZu02IJAsic9sU7B+5uF9cOmqUfeLOdOaAZYb/CA+M/Ic9NxUoYMNfD/PT84f7xB807EAnrrbgMUBZt1w1SEpCIqfjF1Om5EuQNth0iu1r8tPLP76LCpX2yWpHDk2dGH018p6brtD5hOHf04cR3okOTZ0lqPVAW3gVdlMhdrfsTW6drRhDgRrYJcbeKZQxTkenvegNt6YBQwrQvOxG+P3ZHEia9TuClS9Br1XKge8XnxLlxjelzZ/2w4tijDMxyoHIsVQg1zvYPcy7KeZx4jG2zyFakFJF7Whu1XT2QvhfJeryeVNdplYPo4Pi9hKd7VVxVC8O5cH4+N65hXgoKuGfEHmWAskjGxI49Ntu6XHOCAD9ie1PcLSepjDNY00fB8m6KpSyJx/jgg9LfJEfLK40818w+LXY5e5zKaMfKl+DcIlSCZp0cd3U59igDI4+WOa2LunvfvDoD9RrcNLqAjDy3yzfrtKqbAkggSDIZmSlYxzz9a8BaJ101zF2rh3BuSTJaCKGMDEGujHbedXch0X2ebbdEkkDC6a9cQoWVguS53P0JP5xcHY1W/tppD9KxgrdAw5QxnwPn4nOukrPeqkzBJb0m9oJltLtt3a07QYD1IkMAeS7/hw0BXMhzJwXJc/eV7kuiyIN8OOGuUhLP06JUeoxz4FxiZLRouTsDM9WO2OdBRtsIgrzHtk3kgH00JO+cTipc2S9jqyCaluf2xwcnfuB6LndHuEsSzdP4N/gtzoFzSZHRIsaQQiPmidyXgttsnW0YQYDvsh2ROGBPxkMqXjNA/qlCFsnZ8UdlX+kfk0pymlnMWH2JOBfz0sWI+C3OMS1dzPphhPVWHOPC5wdMzIUOzFFHb1lwB2ARF+ZOPt0gshWBPLe/wCRZlu6CIkSei/cE0fD4g2ZbVWceyxH5WPwGvzXrrSTJaDnG7oBoGS3qaCULggCPsv1W5IAd8tzLllJwvpx1WthMIfyg9OVotHy1WVQ4V37wsfgNfkuSZLQcW8Q4lruU/RVbRykrggDXiwwN3uQWnXTa1xMkz2W/on2lndNajpNtAGePw2/MOicBMlqs+8K7GBNbjrFgGe2iX0nUgiAvs+0S2YpgndaFPVRc3SdmVanZlfGjifOiw5PrT/oGvPpG/vDkEH4jZ70Vt86rl5rYimmdP41/s3Uzc4Isup9XNxwvz+0tyNAlONPrtO6hctR+QnluKqNt52O3pxvtClhvxTH0egtmEwbBMlrUxU21OFGtCHKYbavIATv3j90z26kIea4QZRtahfhIuT0anrjH7O3rpjNVHzPIaLG3Lh8Tj5TbRQihjlNyehxTwTLarbZOiiEIcBfbPnGhMtroChXW9JN/VqeYdyPEY4nwwPj6ZCL8C1T+T61JhDqRv8MxZgwlJG2BxzEsrBmgeEzseqt9ti6SNIIA8t6wm901eFDZ66d7M4UkQ56LVgTTvvtKaRqFqoTWymjxGb6LpUzrImYcuzaOIWKJmAptPWpaB2sd+V+yvSB1wB6s7qXgwiUyBpbJdBqFq6MjU18mKCKhRsTyEbx558/wnRmYJzLiV+DYBat6JQ/MX7B1UCxBAKHy3IQrH6W7MhY9MWkUMNAN948/8Mm35/jMDIKlpC3gmBWQtsAjifkE61b36kGQP7DdL7KrVZXnXiYpjYKZxj09Gh7f4kB4yIa/8ZmU1brIIYiYIXaJ3Nbjflv3xBME+DZbSVwIzfIIK89dJkSea18Ihu+XflD9yPztCJnW5Ri5VRntpNh8giVb5ygvBIHu9yaRrchYRO6fFU0CSTPQlDLte6zshx9O3g3D3yJajySd4EDaAsQMsRPaetxk61zty+YTCXRqjf9jO19cOLnyYV+p8QffpcreMXJ7BeRgh77Ds6SIYhGbMBgB2tld1DW0nGL4VxbZfKBbdUHdhol1dl7mOi0MOjttGgWT11lAwU9r1mMSsX0oxwSxgYyWOvKXtiAvBPkV239I7GqZdVqX9FDw2V5+UoYipn2nt/WRMK3LMQlW9poYCZ7WfcrWsdwSBNggMrRYdcLdhjas0+q28lzJOc8bOU7jWLh2AwzEyLxclYm6Z2ZuBEE+YLtTZEVA9tzPdBh5biJ3q5rGD8yRjXbNAPkcm0RuyjTUqf3NQBDge2yHJFaGeDyi4tUD5J3WIXmzs8Y9NDgG3un80OCYIDZCHxqHbJ2iZiEIGmnB8twgzYIkd7vMxiBON59GLJyBQLKMdiM1qOPXyMn2f2f7X5EDdshzkUbhAtED0oZMXCAGiIXgtAW/YXusURdr9NsoufLcgmP20zKy2ErrNSNGRuunMUAshL7zABq61q/RBPkd2yNSn57+X3ZTQZA8t7H3H5p7RwwEt6KP2DrUtAQBIIUsiwt99Kf+tydFntuocVhVRltNWyBTRlumGslopRNkhO1mkRVlLCT3jHYzqyU48WSN+1ZWRou0BZDRyp3Ju9nWnaYnCHA3216JlQWy0gKy557dJSaNQn0nKNL1VrhnwTLavbbOUKsQBBApzzVpFHqsPFdIGoW6AfeG7cMwrcv3TC0io80LQZ5me07kU3WkYqSlhYvkpFGoz8C8bO7RyGjlpi14ztaVliMIIFOeizQKbpI+WdsDGfLcWvcmsaK53b4gdUW3lENZXjxrgrzNdq/IAftohbzzOql4eV/zjUUcu96K7w33KFhGi7rxVisTBEBSxWPiiqYqz71mGfmDQuS5tSIHstHyPZnd7+XKaI+RgKSxEggySWmKaXkVaSwi5xSbRmGiSdZpxVZGy/eEexMso73R1o2WJwiwk+11kQNZrNO6oo+Cc7vz39Wy07q4l+CKfnNvQu/ndVsnSAkifcCOAXq7R8W1y9JdRvI87QvfnTRtgdPeujLavBLkv9meEPnUHS2Tf1EPFT67lOKRnE77munrsrkH/+IeydPXqAO/VoLMDMhz5T2irTzXpFHoKeRPnluV0XYX0mlduTLamIRJtKUR5CDbbSIrGPfX/eUdVFyTQ3luku6OaNIW/HmH5LQFt9k6oAQ5Ab7PNiyxkmGndUhRvTNyJM9F1wrZaM9IZbQmG63MocewxIejRIKg+DaKbEXGI3KWBtT2hUFKyonUZeEfB3xkX4vsM3wXvIx/IwmMqCu0WH/B9qLIpzG6Wp/rpWBFj/x1WnaCAb4G7LPgad0XbZmTEmTukDnti0yzgZvKcwNPtDzXyGjZR5ONFincVEbbVAR5je0hkU/lkTL5F3TZzQ2EvjysJr1hH/0LuiVPTz9ky1oJsgB8iwQsN5hplISns5Hn9hXl9eurMlr2zUzrVsQuk5m0ZUxKkIXhKNsWkQN2yHNPhzx3WbqQMRZGYCOjXWZ8FDzjtsWWsRJkEfgh2zvyOvhWnovsucu75GTPtdlo4RN8i+W+s3nHli0pQRaPIXEeVeW53V46YJciz2Uf4IvxiX0juW/9h/JQ8fJCkGfZnpE5YK9QsHIJBZcIkOdW141d3Gt8EiyjfcaWqRKk6Z84kOc6duODjmzluUZGyz4g6Q18UhltaxHkXbbtIgfsRyvknQt5bobZc6dltP3Gl0SudmW7LUslSJ1mPUbFeWVUepDnDpB3SgazRtW0BXxt+ABfhE7rypyVbCKCTLF9U2QrgjQKg3b7zskGv3eI0+XsuDZ8EJy2YJMtQyVIHfEztldFDtghz728j4LzGphGoZq2gK9ZMDuwiH3ngTJ7OG+VLY8EAeTKc9ts9lwk42zEOi2st+JrYZIA1xYso12Xx4qWV4K8xPZzka3ISCrPDVY1YJ1WtfVYZWW0ctdbPW7LTAnSQHyDJCoykEYhTNdpuUsK6YDZqQ85cG5cw6y3CsWmLYBXG/NayfJMkI8oVR/KG7AfC8k7u4MKVw2kM1r1eB2RpDNXuAauJVhGe6stKyVIBrid7YA4r6o5N5BG4cxOI3mtaeWtymj53LiG4FwmKJs78lzB8k4QVIsN4ryqynN7AzP1ShXIc2tYg3GuSpJO6/aKltHK3KWmhQgCPMm2R+SAfTSkANlzV9Rw2rc6MDcyWtHZaPfYsiElSPaQOYVYiSnxiIprB8kpeGn+v8U2mZD8FjxzTpybKjqtqwQ5Od5g2yGyq4Xsued3UeHSvsW3IlUZLZ8L5xSctmCHLRMliCBgN/AJcV7F6SpbjBe8gUWkUaimLeBzmOUsU2JltOMkcbd+JQiNkYB8ErNVbPe0Nmq72i4kXMiwNUnfe+AcOJfgfCWbbVkoQQTiR2xvivPKynODNX0ULF9AGoVq2gL+Lc4hWEaL2N/XTBWq2Qgic3BYled2+ekeVfOV51az0WKNF59DsIx2XbNVpmYkyPNsuyWSBBJYf+USKsxHnlvNRsu/8WXLaHfb2CtBcoD1Ir2CPJf/wxSt2xmkupGT9c6QtoCPNdO66FfJldGub8aK1KwEeY9tm8gB+2hI3jmdVLii/+RbBdktfHAsfpPIfSm4zcZcCZIjfJftiMQBO1IQQBrrn3qCRYZ20SOOMTLacbHrrRDjW5q1EjUzQbiTTzeIbEUgz+232XNne59RfX+CbLT9omW0iHFFCZJPPMr2W5EDdshzL1tKwfkzrNOqrrfi73CMYBntKzbGpATJL64X6RXWZRVtxlnP+VgaBZO2wEu/wzGatkAJUk+8zLZLZCuCdVoXciux+rhVuXYVMD7Dd7Hc9Va7bGyVIE0Amf3kaXnuIHm9qTwXhr/xmWAZbUXk+E4JsmAcZtsqcsAOee6Z7VS08lwY/sZngmW0W21MlSBNhLvY9onzCqtIxipUuKqf3L6iMfyNz4RO6+6zsWwJ+NRawNvep8S1IhMxucie+8VT0o+6PIqPiB17rG+lCtNqBPkl2wts14gbsCONwqVLzT8Fr7d6wcawZeBS60Hm1GSSTu+a6d5EY6cEyQ5/YLtf4oCd4iQ1ma3H/TZ2SpAWwLfZSqSYK0o2ZqQEaQ1AN32T1vs54yYbMyVIC+GBVuwyLLBL+kCr3rzb4oV/vdZ/jZESZHb8iqS9F5GFp2yMlCAtjCENgcZGCTI79rPdqWH4FO60sVGCKOh7bIc0DNM4ZGNCShAFEFKOsyDVARttTJQgGoJpPMb2Gw2DicFjGgYlyExYpyHQGChBZsfv2B5p4ft/xMZAoQSZFZso3TKo1VC2965QgpwQI2w3t+B932zvXaEEOSnuZtvbQve7196zQgkyZ6zXe1UoQWbH02zPtcB9PmfvVaEEmTeG9B6VIIrZ8RbbvU18f/fae1QoQRYMJKU81oT3dYwkJj1VguQOk9REaY2Pw4323hRKkEVjJ9vrTXQ/r9t7UihBaobr9V6UIIrZ8Wu2J5rgPp6w96JQgtQcG2jmhGl5QWzvQaEEqQsOst2WY/9vs/egUILUtZIN59Dv4ZyTWwmSEyDnUx7luRtJar4qJUjT4RdsL+bI3xetzwolSMOwTn1Vgihmx2tsD+XAz4esrwolSMPxLZK9XGPS+qhQgmSCo2xbBPu3xfqoUIJkhh+yvSPQr3esbwolSOYYUp+UIIrZ8SzbM4L8ecb6pFCC6BNbWw8lSB7wLtt2AX5st74olCDikPWskfRZNSVIi2OKst2+c5P1QaEEEYuH2V7N4Lqv2msrlCDisa5FrqkEUSwIL7E93sDrPW6vqVCC5AaN0l/kVZ+iBGlxfMR2awOuc6u9lkIJkjvcwXagjuc/YK+hUILkEgnVdxeRDfYaCiVIbvEk2546nHePPbdCCZJ7rMvJORVKkEzwBtuOGp5vhz2nQgnSNMBu6uM1OM84Nedu80qQFscY1SYfx2Z7LoUSpOlwH9ubi/j9m/YcCiWIDth1YK4EaUU8z7Z7Ab/bbX+rUII0PdY36DcKJUgu8R7btnkcv83+RqEEaRncwnZkDscdsccqlCAthQrbDXM47gZ7rEIJ0nJ4lO2VE3z/ij1GoQRpWaxb4HcKJUhL4GW2XTN8vst+p1CCtDw+Oc6Y6/hEoQRpCRxm23rcv7fazxRKEIXFXZRuwBDZvxUC4GsIREHflguDkyQqaVYotIulUChBFAoliEKhBFEolCAKhRJEoVCCKBRKEIVCCaJQKJQgCoUSRKFQgigUShCFIhP8vwADACog5YM65zugAAAAAElFTkSuQmCC”;var message = {render: function(){var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c(‘div’,{staticClass:“message”},[_c(’el-row’,{staticClass:“message-test”},[_c(’el-col’,{staticClass:“message-row”,attrs:{“span”:12}},[_c(‘p’,{staticClass:“text”},[_vm._v(“message “+_vm._s(_vm.message)+” – “+_vm._s(_vm.count))])]),_vm._v(” “),_c(’el-col’,{attrs:{“span”:6}},[_c(‘p’,[_vm._v("\n css backgroud image base64\n “)]),_vm._v(” “),_c(‘div’,{staticClass:“tops”}),_vm._v(” “),_c(‘hr’),_vm._v(” “),_c(‘p’,[_vm._v("\n html img 内嵌 image base64\n “)]),_vm._v(” “),_c(‘img’,{staticStyle:{“width”:“100px”},attrs:{“src”:_vm.imgUrl}})])],1)],1)},staticRenderFns: [], name: ‘x-message’, props: { message: String, }, data: function () { return { count: ‘2233hahaha’, imgUrl: logo } }, created: function () { console.log(’logo:’, this.imgUrl); }};message.install = function (Vue) { Vue.component(message.name, message);};export default message;//# sourceMappingURL=index.js.map构建整个组件库在支持按需引入的同时,如果还需支持完整引入整个组件库。则直接在 rollup 配置里加入一个完整的组件库入口rollup.config.js 最终配置// rollup.config.jsconst fs = require(‘fs-extra’);const path = require(‘path’);const pkg = require(”../package.json”)import vue from ‘rollup-plugin-vue2’import postcss from ‘rollup-plugin-postcss’import postcssScss from ‘postcss-scss’import autoprefixer from ‘autoprefixer’import base64 from ‘postcss-base64’import url from ‘rollup-plugin-url’import progress from ‘rollup-plugin-progress’import filesize from ‘rollup-plugin-filesize’;function isDir(dir) { return fs.lstatSync(dir).isDirectory();}const packages = {};const dir = path.join(__dirname, ‘../packages’);const files = fs.readdirSync(dir);files.forEach(file => { const absolutePath = path.join(dir, file); if (isDir(absolutePath)) { packages[file] =packages/${file}/index.js; }});const allScript = ${pkg.name}.allpackages[allScript] = packages/index.js;function createRollupConfig (file, name) { const config = { input: file, output: { file: name === allScript ? lib/index.js : lib/${name}/index.js, format: ’es’, name: name, sourcemap: true }, plugins: [ vue(), postcss({ extract: true, parser: postcssScss, plugins: [ base64({ extensions: [’.png’, ‘.jpeg’], root: ‘./packages/’, }), autoprefixer({ add: true }), ] }), url({ limit: 10 * 1024, //include: [’.svg’] }), progress(), filesize() ] } return config}const buildPackages = []for (let name in packages) { const file = packages[name] buildPackages.push(createRollupConfig(file, name))}export default buildPackages;产出的构建目录:lib ├─message │ index.js | index.css | index.js.map ├─pay │ index.js | index.css | index.js.map ├─share │ index.js | index.css | index.js.map index.js //完整组件库,包含所有组件 index.css index.js.map到这里构建部分完成,下一步,将构建后的lib目录发布到 npm:修改package.json version字段与上次不一样(如: 0.1.2),否则会提交失败修改package.json main字段为: lib/index.js发布:执行 npm publish按需引入组件组件库发布后,我们转入业务项目中npm 安装组件库,如:npm i qw-ui安装完成后,在项目node_modules文件夹下可以找到名为_qw-ui@0.1.1@qw-ui,即我们刚才发布的组件库.完整引入当我们想一次引入整个项目,而非单独引入每次组件时: 修改src/main.js,全局引入整个qw-ui,如:import Vue from ‘vue’import App from ‘./App.vue’import ElementUI from ’element-ui’;import ’element-ui/lib/theme-chalk/index.css’;import qwui from ‘qw-ui’ // 全局引入整个组件库import ‘qw-ui/lib/index.css’ // 全局载入样式Vue.config.productionTip = falseVue.use(ElementUI)Vue.use(qwui)new Vue({ render: h => h(App),}).$mount(’#app’)按需引入按需引入需要借助 babel-plugin-import,我们可以只引入需要的组件,以达到减小项目体积的目的.首先npm install babel-plugin-import –save-dev,然后再项目根目录上新建文件.babelrc.vue cli3 直接修改babel.config.js文件:// babel.config.jsmodule.exports = { presets: [ ‘@vue/app’ ], plugins: [[“import”, { “libraryName”: “qw-ui”, “customName”: (name) => { return ../lib/${name}/index; }, “style”: (name) => { return ${name}.css; } }]]}// src/main.jsimport Vue from ‘vue’import App from ‘./App.vue’import ElementUI from ’element-ui’;import ’element-ui/lib/theme-chalk/index.css’;import { message } from ‘qw-ui’ // 按需引入 message 组件Vue.config.productionTip = falseVue.use(ElementUI)Vue.use(message)new Vue({ render: h => h(App),}).$mount(’#app’)这时候已经启用了 babel-plugin-import ,插件会帮我们将import { message } from ‘qw-ui’转换成 import message from ‘qw-ui/lib/message’ 的写法。同时自动注入组件样式。~ 运行一下项目私有npm业务性组件库一般只适合于公司内部,组件或多或少的也涉及到代码安全和商业风险,所以将打包后的组件库发布到私有npm而不是发布到公网上的npm官网相对要安全很多.私有 npm 仓库可以让我们使用组件就像 npm 官方仓库里的包一样方便。私有 npm 仓库有以下一些特性:私有包托管在内部服务器或者单独的服务器上;可以同步整个官方仓库,也可以只同步需要的;下载的时候,可以让公共包走公共仓库,私有包走私有仓库;可以缓存下载过的包;对于下载,发布,有对应的权限管理。私有npm的搭建有多种方式,最简单的使用 git 仓库作为私有仓库.快速搭建和部署私有的 npm 包管理服务也可以使用 verdaccio对权限、安全性、稳定性有更高要求的可以使用 cnpmjs.org, cnpmjs.org 服务的搭建需要配合数据库使用.完! ...

February 26, 2019 · 4 min · jiezi

全新版本仿网易云音乐来啦

前言在前端技术领域中,我们可以切身感受得到技术的更新、变革的速度是非常快的,所以工程师们都会需要时常关注和学习一些新技术、新标准。因为在工作中负责项目的技术栈相比于业界来说,算比较落后了,所以自己动手来开发一个音乐类 web app,可以尝试一些新技术栈,或者往一些特定方向深挖学习。项目开发时间从年末至今,利用工作之余的时间断断续续地开发,主体功能已经大致完成了,接下来也会陆续添加一些新功能上去,也会持续优化代码,在此也做一下记录和总结。项目信息在线体验使用 chrome 移动端调试体验项目地址技术栈vue:vue 2.6, vue-router, vuex, vue-server-rendererwebpack:webpack 4, webpack-dev-middleware, webpack-hot-middlewarenode:express 4test:karma 4, mocha, sinon-chai, vue/test-utils, eslint整体架构后端 api 是使用 NeteaseCloudMusicApi,提供了非常多接口,并且支持 CORS 跨域。项目分为两个部分,分别是前端,比如 javascript、css、img、components 等;还有服务端,负责请求响应和服务端渲染,所以项目整体架构如图:技术实现项目刚开始使用 vue-cli 初始化,开箱即用的使用体验为我省去了不少繁琐的流程,可以直接上手进行开发。登录态用户登录是首先需要解决的问题,因为许多接口都依赖用户登录态。最终是将 api 服务和项目分成两个子域名:163api.domain.cn // api163music.domain.cn // 项目但是后来发现,请求登录接口成功后,用户 cookie 无法写入到浏览器内,发现原来是 cookie 内的 domain 设置的是 api 子域名,所以导致 163music.domain.cn 下是无法读取到 cookie 的,但是经过调试发现,接口在 set cookie 的时候是并没有设置 domain,解决方案是在 nginx 内加上 proxy_cookie_path 的配置,为 cookie 添加 domain 为 .domain.cn,那么在其他子域名下就能正常读取到 cookie(刚开始设置的是替换 domain,然而不会生效):// nginx.confserver { listen 80; server_name 163api.domain.cn; location / { proxy_pass http://127.0.0.1:3000/; proxy_cookie_path ^(.+)$ “$1; domain=domain.cn”; }}webpack在项目开始初期,一切都是那么的和谐,可以欢腾畅快的开发。开发到中期功能都完成的差不多时候决定添加 ssr 了。vue-cli 3 是可以通过配置文件 vue.config.js 来实现自定义的 webpack 配置,在加入了 ssr 相关配置之后,就可以成功构建打包了,但我希望代码能够实时重载和模块热替换,不然开发效率会比较低下。然后,在尝试了一些改造方案(一番挣扎)之后,还是觉得不能够很灵活地实现,我决定重新搭建环境 Orz主要的 webpack 配置是参考 vue-cli,node 代码主要参考官方 demo,当代码编写好后就尝试运行了,结果当然是…满屏红色报错。因为官方 demo 使用的 webpack 3,所以有些配置需要更新,还有一些依赖随版本升级也需要更新调用方法等等。但值得高兴的是,错误提示都基本是准确的,比如:// 需要提供 mode 选项The ‘mode’ option has not been set, webpack will fallback to ‘production’ for this value. Set ‘mode’ option to ‘development’ or ‘production’ to enable defaults for each environment.// 配置迁移,需要使用新配置选项Error: webpack.optimize.UglifyJsPlugin has been removed, please use config.optimization.minimize instead.还有可能会缺少各种 loader,需要安装各种依赖。确保构建流程正常后,就可以打开浏览器内看效果了,但又会发现这样的报错:window is not defined.原因是因为一个轮播插件内包含 window,而在 node 环境内是没有这个全局变量的,所以导致了这个报错。亦或者访问其他浏览器内置对象时,也会出现这样类似的报错,所以需要确保代码和插件都可以在 node 环境下正常运行。因为轮播插件本身是支持 ssr 模式的,所以修改完代码后即可正常运行。最后,项目中共有四份 webpack 配置和两个构建脚本。在开发环境下,搭配 webpack-dev-middleware 和 webpack-hot-middleware 来实现了代码的热加载。在生产环境构建时,因为希望能清晰看到错误和警告,也想对构建耗时进行统计,所以将构建脚本拎出来。build├── build-production.js // 生产环境构建脚本├── setup-dev-server.js // 开发环境构建脚本├── webpack.base.config.js // 基本配置├── webpack.client.config.js // 客户端配置├── webpack.server.config.js // 服务端配置└── webpack.test.config.js // 测试配置播放器音乐播放是最主要的核心功能,底层就是使用 audio 标签,并且监听了标签元素的 canplay、timeupdate、ended 事件来分别实现时长计算、更新当前播放进度、下一首播放。因为播放器是可以支持“后台”播放,所以将播放器放到根组件中并且设置隐藏,所以 dom 结构如下:<div id=“app”> <!– audio –> <player></player> <transition :name=“transitionName”> <router-view class=“component”></router-view> </transition> <footBox></footBox></div>组件数据同步是使用 vuex,比如播放的状态、歌曲总时长、当前的播放进度等,当歌曲播放完毕时候需要播放下一首,这里使用的是 eventBus 来做事件触发,它会比较适合这种类似的场景。当用户打开播放页面时,我希望音乐是能够自动播放的,无论用户是从其他入口进来亦或者是直接刷新的时候。自动播放是通过 play() 方法去触发的,前者没有问题,但是后者在调用时就会提示错误,错误意思是需要用户进行手势操作之后才能够播放,然后尝试了模拟点击、静音播放的方案之后发现在 chrome 内依然无效,后来感觉 chrome 这样做是正确的,应该把网站的控制权交给用户,让用户清楚页面到底发生了什么,而不是让用户在一堆标签页里寻找是哪个页面发出了奇怪的声音。更新从播放列表进入播放页后才会自动播放,感谢小伙伴提供解决方案单元测试单元测试也是早期规划的功能之一,开始是参考一些开源项目来搭建,最终选型是 karma + mocha + sinon-chai (官方 demo)。搭建的过程就是摸着石头过河了,其中也经历了一些报错,比如:安装依赖失败、配置文件出错、缺少依赖插件等等,然后接近搭建完成后才发现还有官方文档。不得不说是, cli 的确帮开发者节省了非常多配置、搭建的工作,搭建完成之后就可以根据官方文档来编写用例了,根据官方文档内例子已经可以覆盖到绝大部分场景,比如模拟浏览器渲染、用户点击等等。但同时也发现一个问题,如果项目代码经常发生变更的话,那么之前的测试用例也可能需要重新编写了,想知道大家在项目中是怎么处理或者怎么看待呢?以上是在开发过程中遇到一小部分问题,还有过程当中大部分问题描述和解决方案就不在这里一一展开去讲了,大家如果有问题的地方,欢迎大家私信或者邮件与我交流。总结在项目的开发过程中也参考和使用了很多优秀的开源项目,帮助我快速消化一些功能实现,还有提供了后端 api,不然也没有开发这个项目的灵感;Vue 生态下有丰富、详细的官方文档和活跃的社区,基本上遇到的问题都能够解决,超赞;项目在立项之初可能只是大脑一闪而过的简单想法,再回顾这几个月开发经历,其实过得是比较充实和富有激情的,就是有点费头发 ????;最后,自知项目中还有很多不足的地方,如果您发现有什么问题或者有更好的想法,欢迎 issue 或者 pr。如果您觉得项目有参考和学习的价值,可以在 github 上点个 star,谢谢参考资料NeteaseCloudMusicApivue-awesome-swiperuse nginx to add Domain to a Set-CookieCookies on localhost with explicit domainmini-css-extract-plugin with SSRAutoplay Policy Changes ...

February 26, 2019 · 2 min · jiezi

Code Splitting 代码分离

发现问题这是一个基于 vue-cli 的管理后台项目,由于依赖较多,打包结果如下查找原因为什么 vendor 体积这么大?借助 Webpack 的分析工具,看了下各个依赖的体积分布看起来是 Highcharts 和 Element-UI 占了较大体积,那就想办法优化呗这两个库都提供了按需加载的功能,能有效减小体积,只是刚好这个管理后台项目依赖较多解决方法CDN 外链先把 Highcharts 和 Lodash 通过外链引入<script src="//cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.5/lodash.min.js" /><script src="//cdnjs.cloudflare.com/ajax/libs/highcharts/6.0.7/highstock.js" /><script src="//cdnjs.cloudflare.com/ajax/libs/highcharts/6.0.7/js/highcharts-more.js" /><script src="//cdnjs.cloudflare.com/ajax/libs/highcharts/6.0.7/js/modules/treemap.js" />外链引入的资源就不能直接通过 import 来使用,但可以通过 Webpack 的 externals 特性来兼容import _ from ’lodash’import Highcharts from ‘highcharts’console.log([, Highcharts])这样配置 Webpack 就知道这两个依赖是外链全局的,不需要打包externals: { lodash: ‘’, highcharts: ‘Highcharts’}先看看去掉 Highcharts 和 Lodash 的效果vendor 也避免打包了这两个库这种方式适用于不常更新的第三方依赖,采用外链,Element-UI 由于常有新特性更新,我会保持最新版本,所以还是通过 npm 来管理但是,内网部署咋办直到有一天,这个管理后台项目要部署到一个内网机器,访问不了外网,那这种方式就走不通了拆开 vendorWebpack 默认是将依赖打包成一个文件,这样优点是减少资源请求数,但当依赖增多,体积增大,一个资源的加载速度就会减慢所以我开始尝试去拆包new webpack.optimize.CommonsChunkPlugin({ name: ‘charts’, chunks: [‘vendor’], minChunks: module => module.resource.indexOf(‘highcharts’) > -1}),new webpack.optimize.CommonsChunkPlugin({ name: ‘utils’, chunks: [‘vendor’], minChunks: module => module.resource.indexOf(’lodash’) > -1}),new webpack.optimize.CommonsChunkPlugin({ name: ‘ui’, chunks: [‘vendor’], minChunks: module => module.resource.indexOf(’element-ui’) > -1})拆包后的打包结果看看分析工具总结外链简单粗暴,而拆包可以配合浏览器缓存,每次发布最小化更新资源 ...

February 25, 2019 · 1 min · jiezi

进击webpack4 (基础篇:配置 二)

标题文字进击webpack 4 (基础篇一)进击webpack4 (基础篇二:配置)前言:上一节babel配置错误 { test:/.js/, use:{ loader:‘babel-loader’, options:{ presets:[ ‘@babel/preset-env’ ], plugins: [ ["@babel/plugin-proposal-decorators", { “legacy”: true }], ["@babel/plugin-proposal-class-properties", { “loose” : true },"@babel/plugin-transform-runtime"], ] } }},这个才是对的## 多页面配置 ##多页面配置即是多入口entry需要写成对象形式, 注意数组形式会变成多入口单页面, 因为打包之后的chunks 会合并成一个!//webpack.config.js entry:{ home:["@babel/polyfill",’./src/index.js’], login:["@babel/polyfill",’./src/login.js’] }, // 入口文件 出口不能写同一个文件 用[name]代替output:{ filename:’[name].js’, path:path.resolve(__dirname,’./dist’) }以上配置并不能多页面, 还需要2个html模板,并且指明各自的chunks(代码块) plugins: [ // new PluginName 去生成js对象供给 webpack compiler 调用 new HtmlWebpackPlugin({ template:’./index.html’, filename:‘index.html’, chunks:[‘home’] }), new HtmlWebpackPlugin({ template:’./login.html’, filename:’login.html’, chunks:[’login’] }),],否则每个页面会同时引入所有的jsdevtools在production下 打包后的代码都被压缩掉了,我们有时候需要调试代码的时候没法定位,devtools就是干这件事的它有7种类型基本类型eval: 每个module会封装到 eval 里包裹起来执行,并且会在末尾追加注释 //@ sourceURL.source-map: 生成一个SourceMap文件.hidden-source-map: 和 source-map 一样,但不会在 bundle 末尾追加注释.inline-source-map: 生成一个 DataUrl 形式的 SourceMap 文件.eval-source-map: 每个module会通过eval()来执行,并且生成一个DataUrl形式的SourceMap.cheap-source-map: 生成一个没有列信息(column-mappings)的SourceMaps文件,不包含loader的 sourcemap(譬如 babel 的 sourcemap)cheap-module-source-map: 生成一个没有列信息(column-mappings)的SourceMaps文件,同时 loader 的 sourcemap 也被简化为只包含对应行的。我们常用的是source-mapdevtools:‘source-map’watchwatch:true,watchOptions:{ poll:5 // 每秒问5次要不要打包}可以实时监控打包 每当代码变化就重新打包其他的一些plugin clean-webpack-plugin 每次打包之前清空原来的文件夹yarn add clean-webpack-plugin -Dconst cleanWebpackPlugin = require(‘clean-webpack-plugin’)plugins: [ // new PluginName 去生成js对象供给 webpack compiler 调用 new cleanWebpackPlugin(’./dist’) // 指定需要清空的目录], copy-webpack-plugin用于文件的拷贝yarn add copy-webpack-plugin -Dconst copyWebpackPlugin= require(‘copy-webpack-plugin’)plugins: [ // new PluginName 去生成js对象供给 webpack compiler 调用 new copyWebpackPlugin({ from:‘xxx’, to:’./’ // 在dist里 }) // 指定从哪里拷贝到哪里],- webpack 可视化工具yarn add webpack-bundle-analyzer -Dconst BundleAnalyzerPlugin = require(‘webpack-bundle-analyzer’).BundleAnalyzerPlugin;plugins: [ new BundleAnalyzerPlugin () ]跨域代理配置devServer:{ proxy:{ ‘/api’:{ target:‘www.baidu.com’, pathReWrite:{’/api’:’’}, // 请求/api的url /api 会替换成’’ ,并且自动加前缀target secure: false, // 接受运行在 HTTPS 上,使用了无效证书的后端服务器 changeOrigin: true, //虚拟一个服务器接收你的请求并代你发送该请求, } }, contentBase: ‘./dist’, //当前服务以哪个作为静态资源路径 host:’localhost’, // 主机名 默认是localhost port:3000, // 端口配置 open:true, // 是否自动打开网页 compress:true // 是否精简}如需多个代理, 多配制几个proxy的key值就okresolve resolve:{ modules:[path.resolve(’node_modules’)], // 数组 可以配置多个 强制指定寻找第三方模块的目录 使得查找更快 alias:{ //别名配置 import xxx from ‘src/xxx’ —> import xxx from ‘@/xxx’ ‘@’:path.resolve(__dirname,’./src’) }, extensions:[‘css’,‘js’,‘json’,‘jsx’] // 自动添加后缀 加载模块时候依次添加后缀 直到找到为止 } 设置开发或者生产环境webpack自带插件webpack.definePlugin, 可以定义全局变量webpack.definePlugin({ Dev:"‘development’", // 注意双引号里面套的是单引号的字符串 Pro:"‘production’" // if(Dev===‘development’){}else{//…..} 这样在其他环境中使用定义不同接口地址})配置篇完 ...

February 25, 2019 · 2 min · jiezi

前端资源加载重试

介绍对于TO C的应用,用户网络千差万别,总有各种网络问题导致资源加载失败,使得访问时出现白屏,样式错乱等。资源加载重试,则是提高用户体验中重要的一环。最近开始尝试用 Vue 整套技术体系进行开发。如何在 Vue 中做资源加载重试?资源分类目前常见的前端资源分为script 脚本css 样式文件img 图片background-img 背景图而在 webpack 构建体系里,根据加载方式可以细分为内联到html的script,link标签img图片import() 或 require.ensure 异步加载的chunk,通过webpack内置的加载器完成实践方案内联资源重试assets-reload通过 script, link, img 等标签上的 onerror 回调来进行资源加载重试,并且替换的URL规则可定制。而背景图则是读取样式表的规则,匹配到 background-img,则重新插入一条 background-img 样式,用于重试。具体的实现欢迎点击该模块参考。另外配合webpack构建自动化的能力,将这些onerror函数进行绑定。script通过这个模块,再利用script-ext-html-webpack-plugin 配置script的onerror属性 new ScriptExtHtmlWebpackPlugin({ custom: { test: /.js$/, attribute: ‘onerror=“attackCatch(this)”’ } })link另外写个简单的插件将head处内联的link标签加上onerror属性。class MyPlugin { apply (compiler) { compiler.hooks.compilation.tap(‘css-attr-plugin’, (compilation) => { compilation.hooks.htmlWebpackPluginAlterAssetTags .tapAsync(‘myPlugin’, function (data, cb) { data.head.forEach(el=>{ if(el.tagName === ’link’){ el.attributes.onerror = ‘attackCatch(this)’; } }) cb(null ,data); }); }) }}module.exports = MyPluginimgimg目前暂未找到适配的插件,稍后将自行添加对应的插件。也欢迎各位推荐background-img 背景图背景图这一块,则因为没有事件监听,只能进行全量替换,目前的应用仅在测试域名环境下,将所有背景图资源替换为当前域名下。webpack内置异步加载器webpack-plugin-import-retry阅读了webpack资源加载器部分的代码,重写了下加载器部分,实现了重试的能力。同时支持,传入格式化URL函数用于自定义重试时的链接。对加载失败的chunk,进行重试。一个chunk,有时候会包括 JS及CSS资源,其中一个加载失败便会发起重试,直到有一个资源重试了2次就判断为失败。通过资源加载重试,可大大减少 router 中,加载异步的页面文件时,失败而导致白屏的问题。// webpack_require.oldE = webpack_require.e;// webpack_require.e = function newRequireEnsure (chunkId, options) {// return webpack_require.oldE(chunkId, options).then(function () {}, function (err) {// console.error(err);// var type;// if (/..css??/.test(err.request)) {// type = ‘LINK’;// } else if (/..js??./.test(err.request)) {// type = ‘SCRIPT’;// }// if (options === undefined) {// options = {// LINK: 0,// SCRIPT: 0// };// }// options[type]++;// // 最小值为1// if (options[type] <= 2) {// return newRequireEnsure(chunkId, options);// }// })/*****/ }重试规则我们项目中,前端部署的架构为将前端项目文件发布到自己的静态资源服务器,CDN再来进行回源请求文件。URL仅为域名不同,路径相同。因此,我们的重试规则为 加上reloadAssets=1参数,用于标识是第几次重试。第二次重试时,将CDN域名替换为当前域名。因为CDN域名也会有不稳定的时候,将CDN域名替换为当前访问的域名,成功率会高些。因为不同业务的CDN资源替换为主站资源路径未必相同。因此都支持自定义规则。测试域名应用对于测试环境,我们一般会启用一个测试域名用于访问。此时,增量文件尚未发布到CDN,导致访问测试域名时,增量文件请求不到,而为此提前将增量文件发布到线上,则比较麻烦。因此,我们的自定义规则内,会添加是否为测试环境的判断,如果为测试环境,第一次重试的时候就直接替换为当前的测试域名进行访问。以此达到同一套代码适配不同域名。 ...

February 25, 2019 · 1 min · jiezi

利用 Webpack 实现小程序多项目管理

故事是这样的产品小姐姐:“我要做一堆小程序,一周上线一到两个没问题吧”码畜小哥哥:“你他喵是不是傻,做那么多干什么”产品小姐姐:“蹭些流量呀,用户量多了就可以考虑转化流量给公司的 App”码畜小哥哥:“fuck 好的”码畜小哥开始架构小程序杂,放一个项目方便管理小程序多,代码要能够复用团队开发,代码风格要统一码畜小哥开始建项目这是单个小程序的基本目录结构,没问题当一个项目有多个小程序的时候,好像也没问题当多个小程序都用到同一个组件 com3 时,小哥发现代码没法复用,需要复制黏贴思考了一下,那么把组件目录移到外面,这样不就可以复用了吗感觉很好,小哥这时在微信开发者工具打开 demo1,发现报错了原来小程序是以当前项目作为根目录,components 目录已经不在 demo1 目录范围内,所以是引用不到的小哥想到了 Webpack1. 整理目录apps/:存放全部小程序build/:存放构建脚本 common/:存放公共方法components/:存放公共组件styles/:存放公共样式templates/:存放公共模板大概长这样2. 编写构建脚本package.jsonscript: { “dev”: “webpack –config build/webpack.config.js”}build/webpack.config.js思路就是利用 CopyWebpackPlugin 同步指定的文件到小程序目录下const CopyWebpackPlugin = require(‘copy-webpack-plugin’)const utils = require(’./utils’)// 获取 apps 目录下的小程序并指定公共文件目录命名function copyToApps(dir) { let r = [] utils .exec(cd ${utils.resolve('apps')} &amp;&amp; ls) .split(’\n’) .map(app => { r.push({ from: utils.resolve(dir), to: utils.resolve(apps/${app}/_${dir}) }) }) return r}module.exports = { watch: true, // 监听入口文件,保存便会刷新 entry: utils.resolve(‘index.js’), output: { path: utils.resolve(’.tmp’), filename: ‘bundle.js’ }, plugins: [ // 同步指定的公共文件到所有小程序目录下 new CopyWebpackPlugin([ …copyToApps(‘styles’), …copyToApps(‘common’), …copyToApps(’templates’), …copyToApps(‘components’) ]) ]}3. 启动本地开发npm run dev现在公用的代码已经自动同步到小程序目录下,以下划线开头,当改动公共代码也会自动同步给小程序调用调用方式长这样import utils from ‘./_common/utils’import com3 from ‘./_components/com3’@import ‘./_styles/index.wxss’;<import src="./_templates/index.wxml" />代码风格校验package.jsonscript: { “lint”: “eslint apps/”}.eslintrc.jsmodule.exports = { extends: ‘standard’, // 将小程序特有的全局变量排除下 globals: { Page: true, Component: true, App: true, getApp: true, wx: true }, rules: { ‘space-before-function-paren’: [’error’, ’never’], ’no-unused-vars’: [ ’error’, { // 小程序还没支持 ES7,这个是用来兼容 async/await varsIgnorePattern: ‘regeneratorRuntime’ } ] }}然后借助 husky 在每次 git commit 前执行校验script: { “precommit”: “npm run lint”},devDependencies: { “husky”: “^0.14.3”}清理最后小哥还加了个清理命令, 便于重新生成公共代码package.jsonscript: { “clean”: “node build/clean.js”}build/clean.jsconst rimraf = require(‘rimraf’)const utils = require(’./utils’)function log(dir) { console.log(cleaning ${dir})}rimraf(utils.resolve(’.tmp’), () => log(’.tmp’))utils .exec(cd ${utils.resolve('apps')} &amp;&amp; ls) .split(’\n’) .map(app => { ;[ ${app}/_styles, ${app}/_common, ${app}/_templates, ${app}/_components ].map(m => { rimraf(utils.resolve(apps/${m}), () => log(m)) }) })码畜小哥心满意足“可以少加班了” ...

February 25, 2019 · 1 min · jiezi