Webpack学习-工作原理(下)

继上篇文章介绍了Webpack的基本概念,完整流程,以及打包过程中广播的一些事件的作用,这篇文章主要讲生成的chunk文件如何输出成具体的文件。分同步和异步两种情况来分析输出的文件使用的webpack版本:3.8.0。 模块文件show.js function show(content) { window.document.getElementById(‘app’).innerText = ‘Hello,’ + content; } // 通过 CommonJS 规范导出 show 函数 module.exports = show;同步加载// main.jsimport show from ‘./show’;show(‘TWENTY-FOUR K’);生成的bundle文件// webpackBootstrap启动函数// modules存放的是所有模块的数组,每个模块都是一个函数(function(modules) { var installedModules = {}; // 缓存安装过的模块,提升性能 //去传入的modules数组中加载对应moduleId(index)的模块,与node的require语句相似 function webpack_require(moduleId) { // 检查是否已经加载过,如果有的话直接从缓存installedModules中取出 if(installedModules[moduleId]) { return installedModules[moduleId].exports; } // 如果没有的话,就直接新建一个模块,并且存进缓存installedModules var module = installedModules[moduleId] = { i: moduleId, // 对应modules的索引index,也就是moduleId l: false, // 标志模块是否已经加载 exports: {} }; // 执行对应模块函数,并且传入需要的参数 modules[moduleId].call(module.exports, module, module.exports, webpack_require); // 将标志设置为true module.l = true; // 返回这个模块的导出值 return module.exports; } // 储存modules数组 webpack_require.m = modules; // 储存缓存installedModules webpack_require.c = installedModules; // 为Harmony导出定义getter函数 webpack_require.d = function(exports, name, getter) { if(!webpack_require.o(exports, name)) { Object.defineProperty(exports, name, { configurable: false, enumerable: true, get: getter }); } }; // 用于与非协调模块兼容的getdefaultexport函数,将module.default或非module声明成getter函数的a属性上 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; }; // 工具函数,hasOwnProperty webpack_require.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; // webpack配置中的publicPath,用于加载被分割出去的异步代码 webpack_require.p = “”; // 使用__webpack_require__函数去加载index为0的模块,并且返回index为0的模块也就是主入口文件的main.js的对应文件,webpack_require.s的含义是启动模块对应的index return webpack_require(webpack_require.s = 0);})/*********************************************************************/([/ 0 /// (function(module, webpack_exports, webpack_require) {“use strict”;// 设置__esModule为true,影响__webpack_require.n函数的返回值Object.defineProperty(webpack_exports, “esModule”, { value: true });// 同步加载index为1的依赖模块/* harmony import / var WEBPACK_IMPORTED_MODULE_0__show = webpack_require(1);// 获取index为1的依赖模块的export/ harmony import / var __WEBPACK_IMPORTED_MODULE_0__show___default = webpack_require.n(WEBPACK_IMPORTED_MODULE_0__show);// 同步加载__WEBPACK_IMPORTED_MODULE_0__show___default()(‘wushaobin’);// }),/ 1 /// (function(module, exports) {function show(content) { window.document.getElementById(‘app’).innerText = ‘Hello,’ + content;}// 通过 CommonJS 规范导出 show 函数module.exports = show;// })]);异步加载// main.js// 异步加载 show.jsimport(’./show’).then((show) => { // 执行 show 函数 show(‘TWENTY-FOUR K’);});经webpack打包会生成两个文件0.bundle.js和bundle.js,怎么做的原因,是可以吧show.js以异步加载形式引入,这也是分离代码,达到减少文件体积的优化方法,两个文件分析如下。0.bundle.js// 加载本文件(0.bundle.js)包含的模块, webpackJsonp用于从异步加载的文件中安装模块,挂载至全局(bundle.js)供其他文件使用webpackJsonp(// 在其他文件中存放的模块id[0],[// 本文件所包含的模块// (function(module, exports) {function show(content) { window.document.getElementById(‘app’).innerText = ‘Hello,’ + content;}// 通过 CommonJS 规范导出 show 函数module.exports = show;// })]);bundle.js (function(modules) { // webpackBootstrap启动函数 // 安装用于块加载的JSONP回调 var parentJsonpFunction = window[“webpackJsonp”]; // chunkIds 异步加载文件(0.bundle.js)中存放的需要安装的模块对应的chunkId // moreModules 异步加载文件(0.bundle.js)中存放需要安装的模块列表 // executeModules 异步加载文件(0.bundle.js)中存放需要安装的模块安装后需要执行的模块对应的index window[“webpackJsonp”] = function webpackJsonpCallback(chunkIds, moreModules, executeModules) { // 将 “moreModules” 添加到modules对象中, // 将所有chunkIds对应的模块都标记成已经加载成功 var moduleId, chunkId, i = 0, resolves = [], result; for(;i < chunkIds.length; i++) { chunkId = chunkIds[i]; if(installedChunks[chunkId]) { resolves.push(installedChunks[chunkId][0]); } installedChunks[chunkId] = 0; } for(moduleId in moreModules) { if(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) { modules[moduleId] = moreModules[moduleId]; } } if(parentJsonpFunction) parentJsonpFunction(chunkIds, moreModules, executeModules); // 执行 while(resolves.length) { resolves.shift()(); } }; // 缓存已经安装的模块 var installedModules = {}; // 储存每个chunk的加载状态 // 键为chunk的id,值为0代表加载成功 var installedChunks = { 1: 0 }; // 去传入的modules数组中加载对应moduleId(index)的模块,与node的require语句相似,同上,此处省略 function webpack_require(moduleId) { … } // 用于加载被分割出去的需要异步加载的chunk对应的文件, // chunkId需要异步加载的chunk对应的id,返回的是一个promise webpack_require.e = function requireEnsure(chunkId) { // 从installedChunks中获取chunkId对应的chunk文件的加载状态 var installedChunkData = installedChunks[chunkId]; // 如果加载状态为0,则表示该chunk已经加载成功,直接返回promise resolve if(installedChunkData === 0) { return new Promise(function(resolve) { resolve(); }); } // installedChunkData不为空且不为0时表示chunk正在网络加载中 if(installedChunkData) { return installedChunkData[2]; } // installedChunkData为空,表示该chunk还没有加载过,去加载该chunk对应的文件 var promise = new Promise(function(resolve, reject) { installedChunkData = installedChunks[chunkId] = [resolve, reject]; }); installedChunkData[2] = promise; // 通过dom操作,向html head中插入一个script标签去异步加载chunk对应的javascript文件 var head = document.getElementsByTagName(‘head’)[0]; var script = document.createElement(‘script’); script.type = “text/javascript”; script.charset = ‘utf-8’; script.async = true; script.timeout = 120000; // HTMLElement 接口的 nonce 属性返回只使用一次的加密数字,被内容安全政策用来决定这次请求是否被允许处理。 if (webpack_require.nc) { script.setAttribute(“nonce”, webpack_require.nc); } // 文件的路径由配置的publicPath、chunkid拼接而成 script.src = webpack_require.p + "" + chunkId + “.bundle.js”; // 设置异步加载的最长超时时间 var timeout = setTimeout(onScriptComplete, 120000); script.onerror = script.onload = onScriptComplete; // 在script加载和执行完成时回调 function onScriptComplete() { // 防止内存泄漏 script.onerror = script.onload = null; clearTimeout(timeout); var chunk = installedChunks[chunkId]; // 判断chunkid对应chunk是否安装成功 if(chunk !== 0) { if(chunk) { chunk[1](new Error(‘Loading chunk ’ + chunkId + ’ failed.’)); } installedChunks[chunkId] = undefined; } }; head.appendChild(script); return promise; }; // 这里会给__webpack_require__设置多个属性和方法,同上,此处省略 // 异步加载的出错函数 webpack_require.oe = function(err) { console.error(err); throw err; }; // Load entry module and return exports return webpack_require(webpack_require.s = 0); })/*******************************************************************/ ([// 存放没有经过异步加载的,随着执行入口文件加载的模块,也就是同步的模块/ 0 /// (function(module, exports, webpack_require) {// 通过__webpack_require.e异步加载show.js对应的chunk// 异步加载 show.js__webpack_require__.e/* import() /(0).then(webpack_require.bind(null, 1)).then((show) => { // 执行 show 函数 show(‘Webpack’);});/**/ }) ]);总结这里我们通过对比同步加载和异步加载的简单应用,去分析两种情况webpack打包出来文件的差异性,我们发现,对于异步加载的bundle.js与同步的bundle.js基本相似,都是模拟node.js的require语句去导入模块,有所不同的是多了一个__webpack_require__.e函数,用来加载分割出的需要异步加载的chunk对应的文件,以及一个wepackJsonp函数,用于从异步加载的文件中去安装模块。 ...

February 24, 2019 · 3 min · jiezi

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

前文:进击webpack 4 (基础篇 一)webpack.config.js基础配置webpack 有4大概念入口(entry)输出(output)loader插件(plugins)入口与出口//webpack.config.jsconst path = require(‘path’)module.exports = { mode:‘development’, // 指定生产还是开发 entry:’./src/index.js’, // 入口文件 output:{ filename:‘bundle.js’, // 打包后的文件名 path:path.resolve(__dirname,’./dist’) // 这里需指定一个绝对路径 我们需要node的path模块去解析路径 }}mode: 指定环境是development还是production 决定了打包出来的文件是压缩还是未压缩的entry: 入口起点(entry point)指示 webpack 应该使用哪个模块,来作为构建其内部依赖图的开始。进入入口起点后,webpack 会找出有哪些模块和库是入口起点(直接和间接)依赖的。 其中分为单入口跟多入口配置 可以是string,array,object // entry:’./src/index.js’ 是下面的简写 entry:{ main: ‘./src/index.js’ },output:包含打包后的名字跟路径, 如果是多入口应用, 可以设置filename为[name].js, entry里的key值会替换掉name 生成文件夹。loaderloader 用于对模块的源代码进行转换。const path = require(‘path’)module.exports = { mode:‘development’, // 指定生产还是开发 entry:’./src/index.js’, // 入口文件 output:{ filename:‘bundle.js’, // 打包后的文件名 path:path.resolve(__dirname,’./dist’) // 这里需指定一个绝对路径 我们需要node的path模块去解析路径 }, module: { rules: [] // 包含一系列的规则 }}pluginplugin是一个具有 apply 属性的 JavaScript 对象。apply 属性会被 webpack compiler 调用,并且 compiler 对象可在整个编译生命周期访问。// 代码省略module: { rules: [ ] //包含一系列规则 }, plugins: [ // new PluginName 去生成js对象供给 webpack compiler 调用 ]本地开发配置服务yarn add webpack-dev-server -D本地开发需要安装webpack-dev-server 内部是基于express 实现的一个服务 ,可让让服务运行在本地,开发更方便安装完毕在dist目录下新建一个index.html 并且引入打包好后的bundle.js运行npx webpack-dev-server并未显示index.html 需要在webpack-config.js 配置plugins: [ // new PluginName 去生成js对象供给 webpack compiler 调用 ], devServer:{ contentBase: ‘./dist’, //当前服务以哪个作为静态资源路径 host:’localhost’, // 主机名 默认是localhost port:3000, // 端口配置 open:true, // 是否自动打开网页 }重新运行npx webpack-dev-server 自动打开网页并且能正常显示页面如果觉得npx 麻烦的话,可以在package.json 配置脚本"scripts": { “dev”: “webpack-dev-server –config webpack.config.js” }样式文件的处理注意:为了显示效果,不用每次手动修改html 我们先装一个html模板插件yarn add html-webpack-plugin -D webpack-config.js 配置const HtmlWebpackPlugin = require(‘html-webpack-plugin’)//…. plugins: [ // new PluginName 去生成js对象供给 webpack compiler 调用 new HtmlWebpackPlugin({ template:’./index.html’, // 作为模板的文件 filename:‘index.html’ //打包生成后的文件 }) ],进入正题:样式文件分为css,less scss 之类的 需要各种loader 先以css作为 样例需要先安装 style-loader跟css-loadercss的处理yarn add style-loader css-loader -Dwebpack.config.js 配置// 代码省略module: { rules: [ { test:/.css$/, use:[‘style-loader’,‘css-loader’] } ] },rules 里放的是一个个规则对象, test是匹配到的文件 loader是从下往上执行, 从右往左执行, 也就是说命中css结尾的文件后 先用css-loader去解析,解析完毕后交由style-loader 插入到html模板里此处use内部 有2种写法loader:[‘style-loader’,‘css-loader’] // 字符串写法loader:[‘style-loader’,{loader:‘css-loader’,options:{}} 对象写法 在options里可以配置你需求的参数less的处理需要安装less less-loaderyarn add less less-loader -D//webpack-config.js module: { rules: [ { test:/.less$/, use:[‘style-loader’,‘css-loader’,’less-loader’] } ] },sass 配置同理 - 给样式加前缀 如-webkit- 需要安装autoprefixer, postcss-loaderyarn add postcss-loader autoprefixer -D根目录需要新建postcss.config.js// postcss.config.jsmodule.exports = { plugins: [ require(‘autoprefixer’) ]}webpack-config.js rules: [ { test: /.less$/, use: [ “style-loader”, “css-loader”, “less-loader” ] } ]提取样式文件yarn add mini-css-extract-plugin -D//config.jsconst MiniCssExtractPlugin = require(‘mini-css-extract-plugin’)// 注意MiniCssExtractPlugin 不能在development环境中使用 !! //….mode:‘production’, // 指定生产还是开发 module: { rules: [ { test: /.less$/, use: [ MiniCssExtractPlugin.loader, “css-loader”, “less-loader” ] } ] }, plugins: [ // new PluginName 去生成js对象供给 webpack compiler 调用 new HtmlWebpackPlugin({ template:’./index.html’, filename:‘index.html’ }), new MiniCssExtractPlugin({ filename: “[name].css”, chunkFilename: “[id].css” }) ] //… - 压缩提取出来的样式文件 需要用到uglifyjs-webpack-plugin,optimize-css-assets-webpack-pluginyarn add optimize-css-assets-webpack-plugin uglifyjs-webpack-plugin -D//webpack-config.jsmodule.exports = { //… optimization: { // 优化项 这里OptimizeCSSAssetsPlugin 需要UglifyJsPlugin minimizer: [ new UglifyJsPlugin({ cache: true, parallel: true, sourceMap: true // set to true if you want JS source maps }), new OptimizeCSSAssetsPlugin({}) ] },}js文件的处理这里自然轮到我们的babel出场啦 把es6解析为es5 需要这几个loaderyarn add babel-loader @babel/core @babel/preset-env -D { test:/.js/, use:{ loader:‘babel-loader’, options:{ presets:[ ‘@babel/preset-env’ ] } }},es7的语法类似class Parent{ }这种需要@babel/plugin-proposal-class-propertiesyarn add @babel/plugin-proposal-class-properties -D另外装饰器方面需要 yarn add @babel/plugin-proposal-decorators -D { 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 }] ] } }},像一些js内置的api 比如生成器 这种需要yarn add @babel/plugin-transform-runtime -D exclude:/node_modules // 必须配置 不然会解析到node_modules 里的js //…. plugins: [ ["@babel/plugin-proposal-decorators", { “legacy”: true }], ["@babel/plugin-proposal-class-properties", { “loose” : true }] ["@babel/plugin-transform-runtime"] ]还有一些es7 实例上的某些方法 。字符串的include 这些也要转换yarn add @babel/polyfill -D需要添加到entry上或者在入口文件导入全局变量的引入问题有时候我们不想在每个模块都导入某个库或者插件 只需要这样配置 plugins: [ // new PluginName 去生成js对象供给 webpack compiler 调用 new webpack.Provide({ // 自动在每个模块内先导入了React React:‘react’ }),],静态资源的处理yarn add file-loader url-loader -D { test: /.png|jpg|gif$/, use: { loader:‘url-loader’, options:{ limit:2048 // 小于2k 的会用url-loader转为base64 否则file-loader转为真实img outputPath:‘static/image/’ //指定输出目录 }, } },预告: 多页面配置 跨域 区分不同环境 映射 ...

February 24, 2019 · 3 min · jiezi

进击webpack 4 (基础篇一)

主题本文为webpack的基础部分, 旨在如何从0搭建一个工程环境,并简单介绍打包之后生成什么代码, 并且介绍webpack的常用的各种loader,plugin的配置 跟解决了什么问题 本篇为第一篇项目初始化安装webpackyarn init -y个人比较喜欢用yarn初始化项目 完成后yarn add webpack webpack-cli -D再在目录里新建一个src 文件用于放我们的源代码,src创建一个index.js作为本次的入口文件 index.js 随便输入点什么在当前目录下直接运行npx webpack 警告我们需要配置一个mode指明是生产环境还是开发环境打包成功 并且在目录下生成一个dist文件npx 会去找node_modules/.bin/webpack.cmd 文件这个文件就做了一个判断 判断当前目录下有没有node.exe 有就执行当前目录下的webpack.js 没有用node执行上一级的webpack.jswebpack.js 会去找webpack-cli.js 在webpack-cli的文件目录下可以找到config-yargs.js 配置文件 其中一段代码写明了要求什么配置文件.options({ config: { type: “string”, describe: “Path to the config file”, group: CONFIG_GROUP, defaultDescription: “webpack.config.js or webpackfile.js”, requiresArg: true },这里讲明webpack默认配置文件需取名webpack.config.js 或者webpackfile.js ,所以说,我们如果需要用自定义的名字去配置, 需要在命令行里自行添加config 指定webpack用哪个配置去打包 如:npx webpack –config [webpack-config的path] 打包之后的文件在项目根目录下新建文件webpack.config.js 并且导出去const path = require(‘path’)module.exports = { mode:‘development’, // 指定生产还是开发 entry:’./src/index.js’, // 入口文件 output:{ filename:‘bundle.js’, // 打包后的文件名 path:path.resolve(dirname,’./dist’) // 这里需指定一个绝对路径 我们需要node的path模块去解析路径 }}再执行npx webpack 看看这个bundle.js 长什么样子暂时不管__webpack_require__上的属性 简写下就是一个自执行函数传入一个obj 并且在里面自己定义了一个require方法 返回require的执行结果 obj的key是打包文件的path路径, value是一个函数 , 方法体是用eval执行index.js内部的代码。重点就是require做了什么moduleId 就是__webpack_require.s = ‘./src/index.js’定义了一个缓存 {’./src/index.js’:…} 如果存在,也就是打包过了, 则直接返回,如果没有则创建一个对象{ i:moduleId, // 模块的标识 l:false, // 表示是否打包过 exports:{}}并且引用给module,然后用modules[moduleId].call 执行 modules 就是最外部传入的那个自执行函数的参数obj{‘path’:代码块}这段代码就是说 执行这个obj的代码块,上下文指向module.exports, 传入module,module.exports,跟require函数做参数(function(module, exports) { // module对应传入的module(installedModules[moduleId]) ,exports对应module.exports eval(“console.log(‘webpack’)\n\n//# sourceURL=webpack:///./src/index.js?”); }) 这段代码执行了就是输出了’webpack’modules[moduleId].call做的事就是执行传入模块moduleId对应的代码而已 注: 如果此时index.js 内部有import 之类导入其他的模块 其他的模块也会解析成obj的key跟value格式 他会递归去用require解析比如在index.js 里 import a from ‘./a.js’ 他的value是这样的:function(module, exports, require) {“use strict”;eval(“require.r(exports); var a = require("./src/a.js"); // a是require执行后返回的module.exports var a_default = require.n(a); // 返回的是获取模块的函数 console.log(‘webpack’)”); }require.r 就是在exports上定义了个属性作为标识require.n就是执行了require.d 并且返回了一个获取模块的方法,require.d 就是在此函数上添加了 ‘a’, 当访问getter.a 的时候执行getter最后:这样层层递归 一层层打包 最终生成module.exports 返回 module.l 改变true 表示当前这个module已经打包完成 最后返回module.exports下一节预告:webpack.config.js的配置 ...

February 24, 2019 · 1 min · jiezi

webpackPlugin插件总结

功能类html-webpack-plugin自动生成html,基本用法:new HtmlWebpackPlugin({ filename: ‘index.html’, // 生成文件名 template: path.join(process.cwd(), ‘./index.html’) // 模班文件})copy-webpack-plugin拷贝资源插件基本用法:new CopyWebpackPlugin([ { from: path.join(process.cwd(), ‘./vendor/’), to: path.join(process.cwd(), ‘./dist/’), ignore: [’*.json’] }])webpack-manifest-plugin && assets-webpack-plugin俩个插件效果一致,都是生成编译结果的资源单,只是资源单的数据结构不一致而已。webpack-manifest-plugin 基本用法:module.exports = { plugins: [ new ManifestPlugin() ]}assets-webpack-plugin 基本用法:module.exports = { plugins: [ new AssetsPlugin() ]}clean-webpack-plugin在编译之前清理指定目录指定内容。基本用法:// 清理目录const pathsToClean = [ ‘dist’, ‘build’] // 清理参数const cleanOptions = { exclude: [‘shared.js’], // 跳过文件}module.exports = { // … plugins: [ new CleanWebpackPlugin(pathsToClean, cleanOptions) ]}compression-webpack-plugin提供带 Content-Encoding 编码的压缩版的资源基本用法:module.exports = { plugins: [ new CompressionPlugin() ]}progress-bar-webpack-plugin编译进度条插件基本用法:module.exports = { //… plugins: [ new ProgressBarPlugin() ]}代码相关类webpack.ProvidePlugin自动加载模块,如 $ 出现,就会自动加载模块;$ 默认为’jquery’的exports用法:new webpack.ProvidePlugin({ $: ‘jquery’,})webpack.DefinePlugin定义全局常量用法:new webpack.DefinePlugin({ ‘process.env’: { NODE_ENV: JSON.stringify(process.env.NODE_ENV) }})mini-css-extract-plugin && extract-text-webpack-plugin提取css样式,对比:mini-css-extract-plugin 为webpack4及以上提供的plugin,支持css chunkextract-text-webpack-plugin 只能在webpack3 及一下的版本使用,不支持css chunk基本用法 extract-text-webpack-plugin:const ExtractTextPlugin = require(“extract-text-webpack-plugin”); module.exports = { module: { rules: [ { test: /.css$/, use: ExtractTextPlugin.extract({ fallback: “style-loader”, use: “css-loader” }) } ] }, plugins: [ new ExtractTextPlugin(“styles.css”), ]}基本用法 mini-css-extract-plugin:const MiniCssExtractPlugin = require(“mini-css-extract-plugin”);module.exports = { module: { rules: [ { test: /.css$/, use: [ { loader: MiniCssExtractPlugin.loader, options: { publicPath: ‘/’ // chunk publicPath } }, “css-loader” ] } ] }, plugins: [ new MiniCssExtractPlugin({ filename: “[name].css”, // 主文件名 chunkFilename: “[id].css” // chunk文件名 }) ]}编译结果优化类wbepack.IgnorePlugin忽略regExp匹配的模块用法:new webpack.IgnorePlugin(/^./locale$/, /moment$/)uglifyjs-webpack-plugin代码丑化,用于js压缩用法:module.exports = { //… optimization: { minimizer: [new UglifyJsPlugin({ cache: true, // 开启缓存 parallel: true, // 开启多线程编译 sourceMap: true, // 是否sourceMap uglifyOptions: { // 丑化参数 comments: false, warnings: false, compress: { unused: true, dead_code: true, collapse_vars: true, reduce_vars: true }, output: { comments: false } } }] }};optimize-css-assets-webpack-plugincss压缩,主要使用 cssnano 压缩器用法:module.exports = { //… optimization: { minimizer: [new OptimizeCssAssetsPlugin({ cssProcessor: require(‘cssnano’), // css 压缩优化器 cssProcessorOptions: { discardComments: { removeAll: true } } // 去除所有注释 })] }};webpack-md5-hash使你的chunk根据内容生成md5,用这个md5取代 webpack chunkhash。var WebpackMd5Hash = require(‘webpack-md5-hash’); module.exports = { // … output: { //… chunkFilename: “[chunkhash].[id].chunk.js” }, plugins: [ new WebpackMd5Hash() ]};SplitChunksPluginCommonChunkPlugin 的后世,用于chunk切割。webpack 把 chunk 分为两种类型,一种是初始加载initial chunk,另外一种是异步加载 async chunk,如果不配置SplitChunksPlugin,webpack会在production的模式下自动开启,默认情况下,webpack会将 node_modules 下的所有模块定义为异步加载模块,并分析你的 entry、动态加载(import()、require.ensure)模块,找出这些模块之间共用的node_modules下的模块,并将这些模块提取到单独的chunk中,在需要的时候异步加载到页面当中,其中默认配置如下:module.exports = { //… optimization: { splitChunks: { chunks: ‘async’, // 异步加载chunk minSize: 30000, maxSize: 0, minChunks: 1, maxAsyncRequests: 5, maxInitialRequests: 3, automaticNameDelimiter: ‘~’, // 文件名中chunk分隔符 name: true, cacheGroups: { vendors: { test: /[\/]node_modules[\/]/, // priority: -10 }, default: { minChunks: 2, // 最小的共享chunk数 priority: -20, reuseExistingChunk: true } } } }};编译优化类DllPlugin && DllReferencePlugin && autodll-webpack-plugindllPlugin 将模块预先编译,DllReferencePlugin 将预先编译好的模块关联到当前编译中,当 webpack 解析到这些模块时,会直接使用预先编译好的模块。autodll-webpack-plugin 相当于 dllPlugin 和 DllReferencePlugin 的简化版,其实本质也是使用 dllPlugin && DllReferencePlugin,它会在第一次编译的时候将配置好的需要预先编译的模块编译在缓存中,第二次编译的时候,解析到这些模块就直接使用缓存,而不是去编译这些模块。dllPlugin 基本用法:const output = { filename: ‘[name].js’, library: ‘[name]_library’, path: ‘./vendor/’}module.exports = { entry: { vendor: [‘react’, ‘react-dom’] // 我们需要事先编译的模块,用entry表示 }, output: output, plugins: [ new webpack.DllPlugin({ // 使用dllPlugin path: path.join(output.path, ${output.filename}.json), name: output.library // 全局变量名, 也就是 window 下 的 [output.library] }) ]}DllReferencePlugin 基本用法:const manifest = path.resolve(process.cwd(), ‘vendor’, ‘vendor.js.json’)module.exports = { plugins: [ new webpack.DllReferencePlugin({ manifest: require(manifest), // 引进dllPlugin编译的json文件 name: ‘vendor_library’ // 全局变量名,与dllPlugin声明的一致 } ]}autodll-webpack-plugin 基本用法:module.exports = { plugins: [ new AutoDllPlugin({ inject: true, // 与 html-webpack-plugin 结合使用,注入html中 filename: ‘[name].js’, entry: { vendor: [ ‘react’, ‘react-dom’ ] } }) ]}happypack && thread-loader多线程编译,加快编译速度,thread-loader不可以和 mini-css-extract-plugin 结合使用。happypack 基本用法:const HappyPack = require(‘happypack’);const os = require(‘os’);const happyThreadPool = HappyPack.ThreadPool({ size: os.cpus().length });const happyLoaderId = ‘happypack-for-react-babel-loader’;module.exports = { module: { rules: [{ test: /.jsx?$/, loader: ‘happypack/loader’, query: { id: happyLoaderId }, include: [path.resolve(process.cwd(), ‘src’)] }] }, plugins: [new HappyPack({ id: happyLoaderId, threadPool: happyThreadPool, loaders: [‘babel-loader’] })]}thread-loader 基本用法:module.exports = { module: { rules: [ { test: /.js$/, include: path.resolve(“src”), use: [ “thread-loader”, // your expensive loader (e.g babel-loader) “babel-loader” ] } ] }}hard-source-webpack-plugin && cache-loader使用模块编译缓存,加快编译速度。hard-source-webpack-plugin 基本用法:module.exports = { plugins: [ new HardSourceWebpackPlugin() ]}cache-loader 基本用法:module.exports = { module: { rules: [ { test: /.ext$/, use: [ ‘cache-loader’, …loaders ], include: path.resolve(‘src’) } ] }}编译分析类webpack-bundle-analyzer编译模块分析插件基本用法:new BundleAnalyzerPlugin({ analyzerMode: ‘server’, analyzerHost: ‘127.0.0.1’, analyzerPort: 8889, reportFilename: ‘report.html’, defaultSizes: ‘parsed’, generateStatsFile: false, statsFilename: ‘stats.json’, statsOptions: null, logLevel: ‘info’}),stats-webpack-plugin && PrefetchPluginstats-webpack-plugin 将构建的统计信息写入文件,该文件可在 http://webpack.github.io/anal…,并根据分析结果,可使用 PrefetchPlugin 对部分模块进行预解析编译(本人也不理解这个plugin,据说优化效果不明显,有兴趣的同学请见 how-to-optimize-webpacks-build-time-using-prefetchplugin-analyse-tool)。stats-webpack-plugin 基本用法:module.exports = { plugins: [ new StatsPlugin(‘stats.json’, { chunkModules: true, exclude: [/node_modules[\/]react/] }) ]};PrefetchPlugin 基本用法:module.exports = { plugins: [ new webpack.PrefetchPlugin(’/web/’, ‘app/modules/HeaderNav.jsx’), new webpack.PrefetchPlugin(’/web/’, ‘app/pages/FrontPage.jsx’) ];}speed-measure-webpack-plugin统计编译过程中,各loader和plugin使用的时间。const SpeedMeasurePlugin = require(“speed-measure-webpack-plugin”); const smp = new SpeedMeasurePlugin(); const webpackConfig = { plugins: [ new MyPlugin(), new MyOtherPlugin() ]}module.exports = smp.wrap(webpackConfig);转载自:https://segmentfault.com/a/11… ...

February 22, 2019 · 3 min · jiezi

从dist到es:发一个NPM库,我蜕了一层皮

这并不是自己第一次发npm包, 所以这里没有多少入门的知识。在此之前已经有一篇前端脚手架,听起来玄乎,实际呢?,但这一次的npm包和上一次的不是一个概念,前者只是一个脚本工具,而这个npm包是日常开发中方法和组件的集合, 是一个库。在读本文前,假定你已经对npm包有一定概念,熟悉Babel编译和webpack打包的常规用法,知道一些前端工程化的知识。假如你也想自己发布一个npm仓库,但对这一块了解的不是很多,推荐webpack官方的创建一个 library打包模式:日常构建与库的构建在前端日常开发中,引入npm库,执行webpack构建已经是一件不能再平常的事情。但大多数时候,我们不关心这个npm库是怎样构成的,我们只需要知道怎么使用,像antd;在工程化成熟的公司,也不关心webpack的配置到底是怎样的,只需要npm run start或npm run build去启动一次热加载或打包。但是如果你是编写一个npm仓库,这些东西你都需要知道。从那时的无知说起,起初,我用公司的构建工具(类似于roadhog)去打包我的库,没有坎坷,构建出一个2M多的包并成功发布。在测试项目中引入,构建成功。import { EnhanceTable, WithSearch } from ‘antd-doddle’; // 引入仓库在浏览器中打开,打击开始到来:提示要引入的对象没有正确导出,就是没有做module.export,所以这是一个打包模式的问题,output.libraryTarget需要了解一下。webpack的output.libraryTarget决定了打包时对外暴露出来的对象是那种模式,默认是var,用于script标签引入,该模式也是我们日常开发构建最常用的模式,除了这一种,还支持的常用选项有:commonjs(2):node环境CommonJS规范,关于commonjs与commonjs2的区别,commonjs 规范只定义了exports,而 module.exports是nodejs对commonjs的实现,实现往往会在满足规范前提下作些扩展,所以把这种实现称为了commonjs2;amd:amd规范,适用于requireJS;this:通过 this 对象访问(libraryTarget:’this’);window:通过 window 对象访问,在浏览器中(libraryTarget:‘window’)。UMD:将你的 library 暴露为所有的模块定义下都可运行的方式。它将在 CommonJS, AMD 环境下运行,或将模块导出到 global 下的变量,(libraryTarget:‘umd’)。jsonp:这是一种比较特殊的模式,适用于有extrnals依赖的时候(splitChunks)。将把入口起点的返回值,包裹到一个 jsonp 包装容器中。所以在这里我们需要设置两个属性来明确打包模式 library: ‘antd-doddle’, libraryTarget: ‘umd’,2M到38KB, 这中间发什么了什么上图是用roadhog打包出来的结果,其显示的是开启gzip后可以压缩到的大小,第一次打包的实际大小大概在2M(antd+moment+react+css),后面仔细一想,公司的组件库也才300kb啊,自己是不是哪里搞错了,所以接着就有了下面的探寻之路。抽离css(300kb),由于此npm库是基于antd的,所以就没有再把antd的css打包一次的必要了。基于roadhog给予的提示,配置了disableAntdStyle为false,css文件降到2kb;接着上面,虽然是基于antd的,但并没有完全用到antd的所有组件,其官方提供了一个按需打包babel插件babel-plugin-import,并在babelrc中配置, js打包体积由1.6M降为1.2M; [“import”, { “libraryName”: “antd”, “libraryDirectory”: “lib”, “style”: “css”, }]如果对webpack多了解一下,或者在写一个库之前读过 创建一个 library,就会发现前面两点都是白扯没有用的,因为对于这个库来说antd就是一个外部依赖(externals),正好roadhog又支持, 打包出来,由1.2M变为38kb, 这是一个质的提升。 externals: { react: { commonjs: ‘react’, commonjs2: ‘react’, amd: ‘react’, }, antd: { commonjs: ‘antd’, commonjs2: ‘antd’, amd: ‘antd’, }, moment: { commonjs: ‘moment’, commonjs2: ‘moment’, amd: ‘moment’, }, }打包大小优化至此就搞定了,但后面发现用roadhog打包库有一些很难解决的难题,为了解决还得去了解他源码逻辑,所以后面还是自己写了一个webpack,非常简单的配置。ES6之后,光有dist是不够的在写这个库之前,我曾想到在我们日常构建时有下面这样一段配置: rules: [{ test: /.js$/, exclude: /(node_modules|bower_components)/, loader: ‘babel-loader’, query: { presets: [’@babel/preset-env’, ‘@babel/preset-react’] } }这段配置是告诉webpack,node_modules中引用的代码不需要再由babel编译一次,但这些代码还是会被打包进dist文件的。在现在前端的主流开发套路中,被引用的库更希望是一个只编译而没有被打包过的,支持按需加载的库。所以dist中被编译打包过的代码再次被打包, 这样就会有不必要的代码出现。所以这样来看,我们只需要将代码编译。babel,没错,就是它,但是多文件编译,还是找个第三方构建工具比较好,我选择了gulp,直接上代码:// 发布打包gulp.task(’lib’, gulp.series(‘clean’, () => { return gulp.src(’./src/**/*.js’) .pipe(babel()) .pipe(gulp.dest(’./lib’));}, ’lessToLib’)); // lessToLib用于将less文件拷贝贷lib文件夹编译过后大概是下面这样的,确实只编译没打包,函数基本原样:本以为到这就结束了,但是这才开始。只编译不打包消除不必要的代码只是很小的原因,重要的东西我觉得换一行说比较好。不顾语文老师的责骂换行,那什么才是是最重要的:按需打包(tree shaking),对于这种组件和方法库,作为使用者,我们希望他能支持按需打包,像lodash和antd这样。所以怀着好奇的心理我去看了他们的package.json,然后发现了这样的配置: “main”: “lib/index.js”, “module”: “es/index.js”, “name”: “antd”,除了认知中的main入口定义,还多了一个module入口.为什么需要这样呢,和我一样无知的,可以先读webpack官方的tree shaking,如果不够直观,可以再看一位大佬写的一篇相关文章聊聊 package.json 文件中的 module 字段。看下面代码:// es6 模块写法 fun.jsexport function square(x) { return x * x;}export function cube(x) { return x * x * x;}// commonJs写法 fun.jsexports.square = function(x) { return x * x;}exports.cube = function(x) { return x * x * x;}// index.js引入import { square} from ‘./fun.js’;const res = square(10);console.log(‘res:’, res);简单来说,就是index.js打包编译时,引入commonJs写法的fun.js,打包会将square与cube两个函数同时打进来。而引入es6写法的fun.js,只会将square打包。这样的操作,对于现在的主流趋势,就是必须的优化,特别对于lodash和antd这种庞大的库。而要使我们的库支持这样的操作,我们需要编译时,禁止babel将es6的module引入方式编译,其实只需要在前面的基础上多配置一个参数:"@babel/preset-react" // lib的打包方式["@babel/preset-env", { “modules”: false }] // 保留es6模块引入的方式得到的是下面这样的结果:和上面的lib对比,感觉更接近原始代码。至此,编译已结束,但是我们还需要在package.json中加上相应的配置: “description”: “antd后台项目前端组件封装和方法库”, “main”: “lib/index.js”, “module”: “es/index.js”, “scripts”: { “build”: “webpack –config webpack.config.js”, “lib”: “gulp lib”, “es”: “gulp es”, “prepublish”: “gulp && webpack –config webpack.config.js” }, “files”: [ “es”, “dist”, “lib”, “utils” ],怎么让库支持多目录输出因为我的库主要包括组件和方法,我把方法放到一起,通过utils作为默认输出。然后项目中引入是这样的:import { EnhanceTable, WithSearch }, utils from ‘antd-doddle’; // 要用里面的方法需要再分解一次或通过utils.xxxconst { DATE_FORMAT, idCodeValid } = utils;虽然感觉上不复杂,但是总感觉别扭,如果你用过dva,就见过下面这样的引入:import { routerRedux } from ‘dva/router’;import dva from ‘dva’;所以我去学习了一下,发现要这样实现也不难分三步,分目录打包,增加一个输出,并增加内部私有映射,package.json增加一个这个映射目录的输出。具体可查看项目源码。实现后,项目引入是这样的:import { EnhanceTable, WithSearch }, utils from ‘antd-doddle’; import { DATE_FORMAT, idCodeValid } from ‘antd-doddle/utils’; // 一步到位小技巧分享npx执行本地命令以前我们很多命令如webpack,gulp命令只有在全局安装(npm install xxx -g)才可以在命令行中直接运行或在项目中安装,通过script定义执行,但在npm5.2以后,我们可以只项目中安装,然后通过新增的npx执行。比如上面scripts中定义的lib打包(“lib”: “gulp”),我们可以直接在命令行中用:npx gulp命令行切换npm registry有可能你和我一样,在到处都是墙的世界,需要在npm,cnpm,公司的npm registry三者之间来回切换,每次都需要这样:npm set registry ‘https://registry.npm.taobao.org/‘麻烦有没有? 幸好,这世界有很多牛逼的人,nrm registry是个很好用的工具,下面这样:// 安装npm install -g nrm// 设置入口npm,cnpm,companynrm add npm ‘http://registry.npmjs.org’nrm add cnpm ‘https://registry.npm.taobao.org’nrm add vnpm ‘http://npm.company.com’// 切换入口到淘宝入口nrm use cnpm后续一个春节自己断断续续就在倒腾这个,收获还是挺大的。后面自己会慢慢去学习怎么加入demo‘,加入单元测试,去建造一个完整的npm库。源码库:githubnpm仓库地址:npm ...

February 22, 2019 · 2 min · jiezi

webpack 的 scope hoisting 是什么?

原文链接:https://ssshooter.com/2019-02…scope hoisting 是 webpack3 的新功能,直译过来就是「作用域提升」。熟悉 JavaScript 都应该知道「函数提升」和「变量提升」,JavaScript 会把函数和变量声明提升到当前作用域的顶部。「作用域提升」也类似于此,webpack 会把引入的 js 文件“提升到”它的引入者顶部。接下来尝试在 webpack4 使用这个功能,对比启用前后的打包区别,相信你一定能很快理解他的作用。启用插件在 webpack4 中使用 scope hoisting,需要添加 ModuleConcatenationPlugin(模块关联)插件:// webpack.config.jsconst webpack = require(‘webpack’)module.exports = mode => { if (mode === ‘production’) { return {} } return { devtool: ‘source-map’, plugins: [new webpack.optimize.ModuleConcatenationPlugin()], }}文件准备现在已经在开发环境添加上 scope hoisting。但是因为我们希望测试文件引入效果的不同,所以需要添加 4 个文件。// shouldImport.jsexport let sth = ‘something you need’export default { others: ‘’,}// one.js two.js 皆为此代码import other from ‘./shouldImport’let a = otherexport default a// index.jsimport one from ‘./one’import two from ‘./two’let test = ’this is a variable’export default { one, two, test,}文件间的引用关系是这样的:文件都准备好了,立即运行 node_modules/.bin/webpack –mode development 打包文件。这就是 scope hoisting这是打包文件的入口模块部分:{ ‘./src/index.js’: function( module, webpack_exports, webpack_require ) { ‘use strict’ webpack_require.r(webpack_exports) // 关联 ./src/shouldImport.js 模块 let sth = ‘something you need’ /* es6 默认引入 / var shouldImport = { others: ’’ } // 关联 ./src/one.js 模块 let a = shouldImport / es6 默认引入 / var one = a // 关联 ./src/two.js 模块 let two_a = shouldImport / es6 默认引入 / var two = two_a // 关联 ./src/index.js 模块 let test = ’this is a variable’ / es6 默认引入 */ var src = (webpack_exports[‘default’] = { one: one, two: two, test }) } }正常来说 webpack 的引入都是把各个模块分开,通过 webpack_require 导入导出模块(对原理不熟悉的话可以看这里),但是使用 scope hoisting 后会把需要导入的文件直接移入导入者顶部,这就是所谓的 hoisting。可以看出,这么优化后:代码量明显减少减少多个函数后内存占用减少不用多次使用 webpack_require 调用模块,运行速度也会得到提升当然几时你开启了 scope hoisting,webpack 也不会一股脑地把所有东西都堆砌到一个模块。官网对这个问题也清楚地说明了,这里举个例子,在你使用非 ES6 模块或使用异步 import() 时,不会应用作用域提升,模块依然会拆分开,不过具体代码会跟正常的引入有一点差异。 ...

February 21, 2019 · 1 min · jiezi

webpack打包原理(待续)

打包工具要解决的问题:文件依赖管理 梳理文件之间的依赖关系资源加载管理 处理文件的加载顺序(先后时机)和文件的加载数量(合并、嵌入、拆分)效率与优化管理 提高开发效率,完成页面优化

February 21, 2019 · 1 min · jiezi

Webpack系列-第一篇基础杂记

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

February 20, 2019 · 3 min · jiezi

工程优化暨babel升级小记

小记背景随着业务代码的增多,项目代码的编译时长也在增多,遂针对这个痛点在dev下做些优化第一部分:优化dev编译时间这里优化的主要思路是在dev环境下,单独出来一个dll配置文件,将项目中的部分依赖包写入配置文件,最终生成一个在dev环境下专用的dll文件,这样处理的目的是减少开发时的编译时间(ps:经测试可以提升50%左右的编译效率),具体修改如下:独立dev的dll配置拷贝一份当前的dll.config.js文件,并重命名为开发环境专用dll-dev.config.js,并进行如下修改:// dll-dev.config.jsmodule.exports = { entry: { vendor: [ ‘moment’, ’nprogress’, ‘cookie_js’, ’echarts’, ‘jsbarcode’, ’lodash’, ’lodash-decorators’, ‘isomorphic-fetch’, ‘antd’, ‘react’, ‘react-dom’, ‘react-router’, ‘react-router-redux’, ‘redux’, ‘redux-fetch-elegant’, ‘redux-logger’, ‘redux-thunk’ … ] }}修改开发环境配置文件webpack.dev.config.js增加一条package.json命令,用于单独生成dev环境下的dll文件"scripts": { “dll-dev”: “./node_modules/.bin/webpack –config dll-dev.config.js”,}dev环境下执行这条新的命令行生成dll文件以及对应的json映射文件,以便省去dev下一些import进来的包文件编译,从而减少工程的整体编译时长npm run dll-dev第二部分:工程升级到babel@7+升级package依赖包 & 去除冗余and这里删除了’babel-preset-stage-2’,因为为了避免概念模糊不清以及防止出现由于提案的删除或变更而导致不可预见问题,故而babel@7+中删除了阶段预设。其他依赖包从v@6+升级到v@7+,并采用babel@7+中的最新官方包名称。用于antd按需加载的babel-plugin-import也需要跟着babel进行升级到1.11.0,因为配置语法和资源的目录名称都改变了(详见babel.config.js)。修改babel配置文件在babel@7+中,增加了一个新的配置文件babel.config.js,这样可以让配置文件变得更加灵活,更适合babel所采用的monorepo管理,比如可以将配置集中在所有包中、也可以为每一个包单独创建配置,我们这里采用这个配置文件,因为需要在config里写一些判断逻辑,以实现dev和pro的更深层次隔离ps:详细的配置官方说明详见6.x vs 7.x新的babel配置文件如下:// babel.config.jsmodule.exports = function (api) { api.cache(true) const presets = [ ‘@babel/preset-env’, ‘@babel/react’ ] const plugins = [ [’@babel/plugin-transform-runtime’, { ‘helpers’: false, ‘regenerator’: true }], [’@babel/plugin-proposal-class-properties’, { ’loose’: true }], [‘import’, { ’libraryName’: ‘antd’, ’libraryDirectory’: ’lib’, ‘style’: ‘css’ }, ‘ant’] ] return { presets, plugins }}编译测试对比使用同一台电脑及开发环境进行测试比较升级前执行编译耗时如下:升级后执行编译耗时如下:升级前进行修改保存耗时如下:升级后进行修改保存耗时如下: ...

February 19, 2019 · 1 min · jiezi

webpack 最简打包结果分析

原文链接:https://ssshooter.com/2019-02…现在的 webpack 不再是入门噩梦,过去 webpack 最让人心塞的莫过于配置文件,而 webpack4 诞生随之而来的是无配置 webpack。使用 webpack4,至少只需要安装 webpack 和 webpack cli。所以大家完全可以自己打一个最简单的包,还能修改插件对比前后的区别。npm i webpack webpack-cli -D 安装后,因为 webpack4 会默认 src 为入口目录,所以先新建 src/index.js。// src/index.jsimport { sth } from ‘./shouldImport’import other from ‘./shouldImport’let test = ’this is a variable’export default { a: test + ‘,’ + sth, other,}为了更了解 webpack 导入机制所以再新建 src/shouldImport.js。// src/shouldImport.jsexport let sth = ‘something you need’export default { others: ‘’,}然后运行 node_modules/.bin/webpack –mode development 即可在 dist/main.js 看到打包后的文件。但是默认设置中模块文件会被 eval 包裹导致不便查看,所以需要再在设置做一点修改,把 devtool 属性改为 ‘source-map’:// 在根目录新建 webpack.config.js 文件module.exports = mode => { if (mode === ‘production’) { return {} } return { devtool: ‘source-map’, }}然后再打包应该就能看到类似一下的文件结构,开发环境下打包得到的文件自带注释,理解起来不难:;(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 // 执行模块函数(有点不懂为什么 this 要传入 module.exports) modules[moduleId].call( module.exports, // this module, // 模块对象本身 module.exports, // 模块对象的 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 // Object.prototype.hasOwnProperty.call webpack_require.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property) } // define getter function for harmony exports // 为 ES6 export 定义 getter 函数 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 // 于 export 定义 __esModule 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 // value 是模块 id,必要 // mode & 2: merge all properties of value into the ns // 合并 value 所有属性到 ns // mode & 4: return value when already ns object // ns 已经是对象时返回 value // mode & 8|1: behave like require // 表现如 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 // 用于兼容非 ES6 模块的 getDefaultExport 函数 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 } // webpack_public_path webpack_require.p = ’’ // Load entry module and return exports // 加载入口模块并返回 export return webpack_require((webpack_require.s = ‘./src/index.js’))})({ ‘./src/index.js’: /*! exports provided: default / function(module, webpack_exports, webpack_require) { ‘use strict’ webpack_require.r(webpack_exports) // 于 export 定义 __esModule / harmony import */ var shouldImport__WEBPACK_IMPORTED_MODULE_0 = webpack_require( ‘./src/shouldImport.js’ ) let test = ’this is a variable’ /* harmony default export */ webpack_exports[‘default’] = { a: test + ‘,’ + shouldImport__WEBPACK_IMPORTED_MODULE_0[‘sth’], other: shouldImport__WEBPACK_IMPORTED_MODULE_0[‘default’], } }, ‘./src/shouldImport.js’: /*! exports provided: sth, default / function(module, webpack_exports, webpack_require) { ‘use strict’ webpack_require.r(webpack_exports) / harmony export (binding) */ webpack_require.d(webpack_exports, ‘sth’, function() { return sth }) let sth = ‘something you need’ webpack_exports[‘default’] = { others: ‘’, } },})源文件中的所有 import 和 export 都会转换为对应的辅助函数。import 对应 webpack_require__export 对应 webpack_exports[‘default’] 直接赋值和 webpack_require.d。整理一下整个流程:定义 webpack_require 及其辅助函数使用 webpack_require 引入入口模块__webpack_require 函数载入模块,将模块放到模块缓存调用模块同样使用 webpack_require 读取依赖(回到第 3 步)运行模块内部功能使用 webpack_exports[‘default’] 直接赋值和 webpack_require.d 输出运行结束 ...

February 19, 2019 · 3 min · jiezi

使用 webpack 4 和 Babel 7 构建 React 应用及如何引入 Material Design

在过去的一年和一些人中,我一直在与 Creative Tim 合作。 我一直在使用 create-react-app 来开发一些不错的产品。 有很多客户询问如何在 Webpack 上迁移我们的产品模板。在多次要求求之后,我们写了这个关于如何开始使用 React with Webpack 4和 Babel 7 的小教程。在本教程的最后,将向大家展示如何在新创建的应用程序上添加 Material Dashboard React。在我们开始之前,请确保你的电脑上安装了 npm 和 Nodejs 的最新版本。在撰写本文时,我的电脑上的最新版本是 npm 的 6.4.1 和 Nodejs 的 8.12.0 (lts)。建立项目首先,创建工程目录:mkdir react-webpack-babel-tutorialcd react-webpack-babel-tutorial现在我们已经创建了我们要开发应用程序的文件夹,接着需要添加一个 package.json 文件。 有两种创建方式,你可以选择其中的一种:1.只需创建 package.json 文件,无需任何其他配置:npm init -y如下所示,package.json 文件已创建,其中包含一些非常基本的信息。2.使用一些额外的配置设置创建 package.json 文件npm init我在我们新创建的 package.json 文件中添加了一些东西,比如一些很好的 keywords,一个repo等等…之后,在根目录下创建 src 文件夹,然后在 src 下分别创建 index.html 和 index.js.1.Linux / MacOS 命令mkdir srctouch src/index.htmltouch src/index.js2.Windows 命令mkdir srcecho "" > src\index.htmlecho "" > src\index.js创建完后在 index.html 添加以下内容。<!DOCTYPE html><html lang=“en”> <head> <meta charset=“utf-8”> <meta name=“viewport” content=“width=device-width, initial-scale=1, shrink-to-fit=no”> <meta name=“theme-color” content="#000000"> <title>React Tutorial</title> </head> <body> <noscript> You need to enable JavaScript to run this app. </noscript> <div id=“root”></div> <!– This HTML file is a template. If you open it directly in the browser, you will see an empty page. You can add webfonts, meta tags, or analytics to this file. The build step will place the bundled scripts into the <body> tag. –> </body></html>接着在 index.js 中添加一些内容,目前只是为了展示一些内容,下面继续完善。(function () { console.log(“hey mister”);}());此时,目录结构如下:将 Webpack 添加到项目中安装 Webapck 及所需的开发环境依赖模块。npm install –save-dev webpack webpack-cli webpack-dev-serverwebpack用来配置我们的新应用本文所用的版本是 4.19.0webpack-cli可以在命令行中使用 Webpack 了本文所用的版本是 3.1.0webpack-dev-server这样,当我们对新应用程序中的文件进行更改时,就不需要刷新页面了。每当我们在应用程序中更改文件时,它会自动刷新浏览器页面本文所用的版本是 3.1.8看一下package.json文件,将看到这三个包被添加到这个文件中,如下所示:“devDependencies”: { “webpack”: “^4.19.0”, “webpack-cli”: “^3.1.0”, “webpack-dev-server”: “^3.1.8”}补充说明一下版本号前的 ^,~ 的区别指定版本:比如"webpack": “4.19.0”,表示安装 4.19.0 的版本波浪号 ~ 指定版本:比如 “webpack-cl”: “~3.1.0”,表示安装 3.1.0 的最新版本(不低于1.1.0),但是不安装 1.2.x,也就是说安装时不改变大版本号和次要版本号^ 指定版本:比如 “webpack-dev-server”: “^3.1.8”,,表示安装 请输入代码3.1.4 及以上的版本,但是不安装4.0.0,也就是说安装时不改变大版本号。package.json 文件只能锁定大版本,也就是版本号的第一位,并不能锁定后面的小版本,你每次 npm install 都是拉取的该大版本下的最新的版本,为了稳定性考虑我们几乎是不敢随意升级依赖包的,这将导致多出来很多工作量,测试/适配等,所以 package-lock.json 文件出来了,当你每次安装一个依赖的时候就锁定在你安装的这个版本。所以当我们 npm install 安装完插件后,都会生成两个文件一个是 node_modules 和 package-lock.json 。这里为了版本的一致性,我把 package.json 里的 版本号前面 ^ 删除了。接着新建 Webpack 配置文件 webpack.config.js:1.Linux/MacOS 命令touch webpack.config.js2.Windows 命令echo "" > webpack.config.js如果你不想使用命令行,也可以简单地手动创建文件。在开始处理 Webpack 配置文件之前,先在应用程序中安装一些我们需要的东西。首先安装 path 作为开发环境的路径依赖。npm install –save-dev path此外,由于不想在 HTML 中手动注入 index.js 文件,因此需要安装 html-webpack-plugin 的插件。 此插件通过配置在 HTML 文件中注入 index.js,无需手动操作。npm install –save-dev html-webpack-plugin再次,我将 package.json 文件删除所有 ^。安装完后在 package.json 文件中的 scripts 属性里添加以为内容:“webpack”: “webpack”,“start”: “webpack-dev-server –open"现在 package.json 内容如下:{ “name”: “react-webpack-babel-tutorial”, “version”: “1.0.0”, “description”: “This is a Tutorial to showcase the usage of React with Webpack and Babel”, “main”: “index.js”, “scripts”: { “test”: “echo "Error: no test specified" && exit 1”, “webpack”: “webpack”, “start”: “webpack-dev-server –open” }, “repository”: { “type”: “git”, “url”: “git+https://github.com/creativetimofficial/react-webpack-babel-tutorial.git” }, “keywords”: [ “react”, “webpack”, “babel”, “creative-tim”, “material-design” ], “author”: “Creative Tim <hello@creative-tim.com> (https://www.creative-tim.com/)", “license”: “MIT”, “bugs”: { “url”: “https://github.com/creativetimofficial/react-webpack-babel-tutorial/issues" }, “homepage”: “https://github.com/creativetimofficial/react-webpack-babel-tutorial#readme", “devDependencies”: { “html-webpack-plugin”: “3.2.0”, “path”: “0.12.7”, “webpack”: “4.19.0”, “webpack-cli”: “3.1.0”, “webpack-dev-server”: “3.1.8” }}接着运行以下命令,看看会发生什么:npm run webpackWebpack 将自动获取 src/index.js 文件,编译它,并将其输出到 dist/main.js 中,并压缩代码。这是因为我们还没有配置 Webpack 配置文件。此外,由于我们还没有配置该文件,我控制台中将出现一些警告。如果我们运行如下命令:npm startwebpack-dev-server 将自动启动服务器并使用该服务器打开默认浏览器。但是,由于我们没有配置webpack.config.js 文件,所以页面展示并不是我们想要的内容。如果想停止服务,只需在命令行中同时按下 CTRL + C 键。接着在 webpack.config.js 添加以下内容:const path = require(‘path’);const HtmlWebpackPlugin = require(‘html-webpack-plugin’);module.exports = { entry: path.join(__dirname,‘src’,‘index.js’), output: { path: path.join(__dirname,‘build’), filename: ‘index.bundle.js’ }, mode: process.env.NODE_ENV || ‘development’, resolve: { modules: [path.resolve(__dirname, ‘src’), ’node_modules’] }, devServer: { contentBase: path.join(__dirname,‘src’) }, plugins: [ new HtmlWebpackPlugin({ template: path.join(__dirname,‘src’,‘index.html’) }) ]};1.entryentry 可以是个字符串或数组或者是对象。 当 entry 是个字符串的时候,用来定义入口文件:entry: ‘./js/main.js’当 entry 是个数组的时候,里面同样包含入口js文件,另外一个参数可以是用来配置webpack提供的一个静态资源服务器,webpack-dev-server。webpack-dev-server 会监控项目中每一个文件的变化,实时的进行构建,并且自动刷新页面:entry: [ ‘webpack/hot/only-dev-server’, ‘./js/app.js’]当 entry 是个对象的时候,我们可以将不同的文件构建成不同的文件,按需使用,比如在我的 hello 页面中只要\ 引入 hello.js 即可: entry: { hello: ‘./js/hello.js’, form: ‘./js/form.js’ }2.outputoutput 参数是个对象,用于定义构建后的文件的输出。其中包含 path 和 filename:output: { path: ‘./build’, filename: ‘bundle.js’ }3.mode这是输出的模式,这里将其 mode 设置为 “development”。如果在脚本中指定 NODE_ENV 变量,那么它将使用这个变量。请参阅下面关于如何使用 NODE_ENV 的示例(请注意,本教程中的 package.json 文件中不会进行以下更改,这只是一个示例,可以看到它是如何工作的)“webpack”: “NODE_ENV=production webpack”,4.resolvewebpack 在构建包的时候会按目录的进行文件的查找,resolve 属性中的 extensions 数组中用于配置程序可以自行补全哪些文件后缀:resolve:{ extensions:[’’,’.js’,’.json’]}5.devServer这告诉 webpack-dev-server 需要提供哪些文件。 这里是 src 文件夹中的所有内容都需要在浏览器中浏览。6.plugins在这里,我们设置了我们的应用程序中需要的插件。到目前为止,只需要 html-webpack-plugin,它告诉服务器 index.bundl.js 应该被注入到 index.html 文件中再次运行以下命令,显示会跟上一次不同:npm run webpackwebpack-dev-server 从 src 文件夹中读取所有内容并输出到我们的浏览器中。配置 React,Babel 与 styles loaders通过运行以下命令来引入 React :npm i react react-dom –save-dev在我们的开发过程中,如果我们在 JS 文件中添加React代码,Webpack 会给我们一个错误,它不知道如何在bundle.js 文件中编译 React。修改 index.js 内容如下:import React from “react”;import ReactDOM from “react-dom”;let HelloWorld = () => { return <h1>Hello there World!</h1>}ReactDOM.render( <HelloWorld/>, document.getElementById(“root”));再次运行以下命令:npm start错误如下:所以这就是 Babel 出现的原因, Babel 将告诉 Webpack 如何编译 React 代码。安装 Babel 相关依赖:npm install –save-dev @babel/core @babel/node @babel/preset-env @babel/preset-react babel-loader@babel/core这是将ES6及以上版本编译成ES5@babel/nodebabel-node 是 babel-cli 的一部分,它不需要单独安装。它的作用是在 node 环境中,直接运行 es2015 的代码,而不需要额外进行转码。例如我们有一个 js 文件以 es2015 的语法进行编写(如使用了箭头函数)。我们可以直接使用 babel-node es2015.js 进行执行,而不用再进行转码了。可以说:babel-node = babel-polyfill + babel-register@babel/preset-react这个是把 React 代码编译成 ES5 代码。babel-loader和 babel-cli 一样,babel-loader 也会读取 .babelrc 或者 package.json 中的 babel 段作为自己的配置,之后的内核处理也是相同。唯一比 babel-cli 复杂的是,它需要和 webpack 交互,因此需要在 webpack 这边进行配置。比较常见的如下:module: { rules: [ { test: /.js$/, exclude: /(node_modules|bower_components)/, loader: ‘babel-loader’ } ]}如果想在这里传入 babel 的配置项,也可以把改成:// loader: ‘babel-loader’ 改成如下:use: { loader: ‘babel-loader’, options: { // 配置项在这里 }}我们需要为项目中添加一些样式,此时就需要使用样式相关的加载器,这边使用 scss,安装命令如下:npm install –save-dev style-loader css-loader sass-loader node-sassstyle-loader通过注入 <style> 标签将 CSS 添加到 DOM 中css-loadercss-loader用于将 css 文件打包到js中, 常常配合 style-loader 一起使用,将 css 文件打包并插入到页面中sass-loader加载 SASS/SCSS 文件node-sass将 SCSS 文件编译为 CSS 文件在 src 下创建 scss 文件:1.Linux/MacOS 命令touch src/index.scss2.window 命令echo "” > src/index.scss并添加以下内容:body { div#root{ background-color: #222; color: #8EE4AF; }}接着导入 index.js 中 import React from “react”; import ReactDOM from “react-dom”; // this line is new // we now have some nice styles on our react app import “index.scss”; let HelloWorld = () => { return <h1>Hello there World!</h1> } ReactDOM.render( <HelloWorld/>, document.getElementById(“root”) );记得删除 package.json 中的 ^ 号,内容如下: { “name”: “react-webpack-babel-tutorial”, “version”: “1.0.0”, “description”: “This is a Tutorial to showcase the usage of React with Webpack and Babel”, “main”: “index.js”, “scripts”: { “test”: “echo "Error: no test specified" && exit 1”, “webpack”: “webpack”, “start”: “webpack-dev-server –open” }, “repository”: { “type”: “git”, “url”: “git+https://github.com/creativetimofficial/react-webpack-babel-tutorial.git” }, “keywords”: [ “react”, “webpack”, “babel”, “creative-tim”, “material-design” ], “author”: “Creative Tim <hello@creative-tim.com> (https://www.creative-tim.com/)", “license”: “MIT”, “bugs”: { “url”: “https://github.com/creativetimofficial/react-webpack-babel-tutorial/issues" }, “homepage”: “https://github.com/creativetimofficial/react-webpack-babel-tutorial#readme", “devDependencies”: { “@babel/core”: “7.0.1”, “@babel/node”: “7.0.0”, “@babel/preset-env”: “7.0.0”, “@babel/preset-react”: “7.0.0”, “babel-loader”: “8.0.2”, “css-loader”: “1.0.0”, “html-webpack-plugin”: “3.2.0”, “node-sass”: “4.9.3”, “path”: “0.12.7”, “sass-loader”: “7.1.0”, “style-loader”: “0.23.0”, “webpack”: “4.19.0”, “webpack-cli”: “3.1.0”, “webpack-dev-server”: “3.1.8” }, “dependencies”: { “react”: “16.5.1”, “react-dom”: “16.5.1” }}如果我们再次运行上述任何命令,错误仍将存在。 我们还没有告诉 Webpack 它应该使用 Babel 和样式加载器来编译我们的 React 和 SCSS 代码。接下来要做的是为 Babel 添加配置文件。 为此,需要创建一个名为 .babelrc 的文件,用来配置 Babel。可以直接在 webpack.config.js 文件中添加 Babel 的配置。 为此,你可以查看官方的 babel-loader 文档。 就我而言,我认为最好将 Babel 配置放在自己的文件中,这样就不会使 Webpack 配置过于复杂难读。在根目录下创建 .babelrc1.Linux/MacOS 命令touch .babelrc2.Windows 命令echo "” > .babelrc并在 .babelrc 中添加以下代码,这样 babel-loader 就会知道用什么来编译代码:{ “presets”: [ “@babel/env”, “@babel/react” ]}完成这些步骤后,我们需要在项目中添加一些内容,以便我们可以导入各种文件,如图像。 还需要添加一个插件,让我们可以使用类等等。 让我们在类中添加类属性,基本上,它将让我们能够使用 面向对象编程 方式来编写代码。因此,让我们将 webpack.config.js 更改为以下内容(我也添加了一些注释,可能会对你有所帮助):// old// const path = require(‘path’);// const HtmlWebpackPlugin = require(‘html-webpack-plugin’);// newimport path from ‘path’;import HtmlWebpackPlugin from ‘html-webpack-plugin’;module.exports = { entry: path.join(__dirname,‘src’,‘index.js’), output: { path: path.join(__dirname,‘build’), filename: ‘index.bundle.js’ }, mode: process.env.NODE_ENV || ‘development’, resolve: { modules: [path.resolve(__dirname, ‘src’), ’node_modules’] }, devServer: { contentBase: path.join(__dirname,‘src’) }, module: { rules: [ { // 这样我们就可以将React、ES6及以上的任何内容编译成正常的ES5语法 test: /.(js|jsx)$/, // 不希望编译node_modules中的任何内容 exclude: /node_modules/, use: [‘babel-loader’] }, { test: /.(css|scss)$/, use: [ “style-loader”, // creates style nodes from JS strings “css-loader”, // translates CSS into CommonJS “sass-loader” // compiles Sass to CSS, using Node Sass by default ] }, { test: /.(jpg|jpeg|png|gif|mp3|svg)$/, loaders: [‘file-loader’] } ] }, plugins: [ new HtmlWebpackPlugin({ template: path.join(__dirname,‘src’,‘index.html’) }) ]};我们需要对 package.json 文件做一个更改。我们需要告诉我们的脚本在 Webpack 的配置文件中,使用 import 而不是 require 语句。 否则它会给我们一个错误,它不知道import 表示什么。{ “name”: “react-webpack-babel-tutorial”, “version”: “1.0.0”, “description”: “This is a Tutorial to showcase the usage of React with Webpack and Babel”, “main”: “index.js”, “scripts”: { “test”: “echo "Error: no test specified" && exit 1”, “webpack”: “babel-node ./node_modules/webpack/bin/webpack”, “start”: “babel-node ./node_modules/webpack-dev-server/bin/webpack-dev-server –open” }, “repository”: { “type”: “git”, “url”: “git+https://github.com/creativetimofficial/react-webpack-babel-tutorial.git” }, “keywords”: [ “react”, “webpack”, “babel”, “creative-tim”, “material-design” ], “author”: “Creative Tim <hello@creative-tim.com> (https://www.creative-tim.com/)", “license”: “MIT”, “bugs”: { “url”: “https://github.com/creativetimofficial/react-webpack-babel-tutorial/issues" }, “homepage”: “https://github.com/creativetimofficial/react-webpack-babel-tutorial#readme", “devDependencies”: { “@babel/core”: “7.0.1”, “@babel/node”: “7.0.0”, “@babel/plugin-proposal-class-properties”: “7.0.0”, “@babel/preset-env”: “7.0.0”, “@babel/preset-react”: “7.0.0”, “babel-loader”: “8.0.2”, “css-loader”: “1.0.0”, “file-loader”: “2.0.0”, “html-webpack-plugin”: “3.2.0”, “node-sass”: “4.9.3”, “path”: “0.12.7”, “sass-loader”: “7.1.0”, “style-loader”: “0.23.0”, “webpack”: “4.19.0”, “webpack-cli”: “3.1.0”, “webpack-dev-server”: “3.1.8” }, “dependencies”: { “react”: “16.5.1”, “react-dom”: “16.5.1” }}我们还需要在 .babelrc 文件中添加 @babel/plugin-proposal-class 属性,Babel将会知道如何处理类属性。{ “presets”: [ “@babel/env”, “@babel/react” ], “plugins”: [ “@babel/plugin-proposal-class-properties” ]}现在总算配置完成了。再次运行上述任何一个命令,就可以顺利跑起来啦。npm run webpacknpm start使用 Webpack 和 Babel 项目将 Material Design 加到我们的新 React 项目中正如在这篇文章的开头讲的,我们不会讲 Material Design 样式如何写,这需要大量的工作。相反,这里添加一个很好的产品来实现 Google 的 Material Design,其中包括Creative Tim 员工的一些小改动。 我们将向其添加 Material Dashboard React。首先从 github 上把项目拷贝下来:git clone https://github.com/creativetimofficial/material-dashboard-react.gitDownload from Github好的,现在我们有两个项目 - Material Dashboard React 和 我们刚创建的项目。现在,我们不能简单地将 src 文件夹从 Material Dashboard React 复制到我们的新项目中。 这会给我们带来很多错误, 如缺少依赖关系的错误,找不到模块等。因此,我建议首先将 Material Dashboard React 的 package.json 中的依赖项添加到 package.json 中。 我们不需要 Material Dashboard React 包中的所有依赖项,因为我们使用 Webpack 构建了自己的服务器。 除了产品本身,我们还添加了其他样式加载器。所以说,我们需要如下:npm install –save @material-ui/core@3.1.0 @material-ui/icons@3.0.1 @types/googlemaps@3.30.11 @types/markerclustererplus@2.1.33 chartist@0.10.1 classnames@2.2.6 perfect-scrollbar@1.4.0 react-chartist@0.13.1 react-google-maps@9.4.5 react-router-dom@4.3.1 react-swipeable-views@0.12.15我们不会全部都讲,你可以在 npmjs.com 上找到它们的详细信息和它们自己的文档。再一次,进入 package.json 文件并从我们刚刚安装的安装包中删除(^)。接着拷贝 Material Dashboard React src 下的所有文件到我们项目 src 下好了,差不多做完了,我们拷贝 Material Dashboard React 下的 src文件中所有内容到我们项目 src 下,并覆盖 index.js 文件。但是要保留 index.html 文件。拷贝前拷贝后现在需要在 index.html 中添加一些样式和字体,如下:<!DOCTYPE html><html lang=“en”> <head> <meta charset=“utf-8”> <meta name=“viewport” content=“width=device-width, initial-scale=1, shrink-to-fit=no”> <meta name=“theme-color” content="#000000”> <link rel=“stylesheet” href=”//cdn.jsdelivr.net/chartist.js/latest/chartist.min.css”> <script src=”//cdn.jsdelivr.net/chartist.js/latest/chartist.min.js”></script> <link rel=“stylesheet” href=“https://fonts.googleapis.com/css?family=Roboto:300,400,500,700|Material+Icons”> <link href=“https://fonts.googleapis.com/icon?family=Material+Icons" rel=“stylesheet”> <title>React Tutorial</title> </head> <body> <noscript> You need to enable JavaScript to run this app. </noscript> <div id=“root”></div> <!– This HTML file is a template. If you open it directly in the browser, you will see an empty page. You can add webfonts, meta tags, or analytics to this file. The build step will place the bundled scripts into the <body> tag. –> </body></html>还有一个小问题。 当我们刷新页面时,有一个错误 Cannot GET/dashboard。 如果我们跳转到另一个页面,会得到如, Cannot GET /user错误等 。 所以根本上,我们的路由不起作用,需要在 src/index.js 或 webpack.config.js 中进行一些更改。这里选择使用第一个方案,因为它非常简单易懂。我们在新导航方式在 index.js 更改 history,使用 createHashHistory() 替换 createBrowserHistory()。这将允许我们刷新页面而不会出现任何其他错误,现在我们完成了。import React from “react”;import ReactDOM from “react-dom”;import { createHashHistory } from “history”;import { Router, Route, Switch } from “react-router-dom”;import “assets/css/material-dashboard-react.css?v=1.5.0”;import indexRoutes from “routes/index.jsx”;const hist = createHashHistory();ReactDOM.render( <Router history={hist}> <Switch> {indexRoutes.map((prop, key) => { return <Route path={prop.path} component={prop.component} key={key} />; })} </Switch> </Router>, document.getElementById(“root”));原文:https://medium.freecodecamp.o…你的点赞是我持续分享好东西的动力,欢迎点赞!一个笨笨的码农,我的世界只能终身学习!更多内容请关注公众号《大迁世界》! ...

February 18, 2019 · 7 min · jiezi

前端prerender-spa-plugin预渲染

prerender-spa-plugin预渲染:构建阶段生成匹配预渲染路径的 html 文件(注意:每个需要预渲染的路由都有一个对应的html)。构建出来的 html 文件已有部分内容。prerender-spa-plugin使用安装prerender-spa-pluginnpm install prerender-spa-plugin –savewebpack.prod.config.js中引入const PrerenderSPAPlugin = require(‘prerender-spa-plugin’) const Renderer = PrerenderSPAPlugin.PuppeteerRendererplugins: [new PrerenderSPAPlugin({staticDir: path.join(__dirname, ‘../dist’), // outputDir: path.join(__dirname, ‘../prerendered’),indexPath: path.join(__dirname, ‘../dist’, ‘index.html’), routes: [’/mobile/home.html’, ‘/mobile/doctor_team.html’, ‘/mobile/about.html’], renderer: new Renderer({inject: {foo: ‘bar’},headless: false, renderAfterDocumentEvent: ‘render-active’ // renderAfterElementExists: ‘.container’, // renderAfterTime: 5000 })})]staticDir:代码打包目录indexPath:模板页面routes:要预渲染的页面路由inject:默认挂在window.__PRERENDER_INJECTED对象上,可以通过window.__PRERENDER_INJECTED.foo在预渲染页面取值headless:渲染时显示浏览器窗口。对调试很有用。renderAfterDocumentEvent:等到事件触发去渲染,此处我理解为是Puppeteer获取页面的时机renderAfterDocumentEvent 这个则很关键,这个是监听 document.dispatchEvent事件,决定什么时候开始预渲染。需要在钩子函数中触发事件,如new Vue({ el: ‘#app’, router, render: h => h(App), mounted () { // You’ll need this for renderAfterDocumentEvent. document.dispatchEvent(new Event(‘render-active’)) }});renderAfterElementExists:等到dom元素出现时去渲染renderAfterTime:5000ms后去渲染webpack打包编译结合项目,执行打包编译命令npm run build:prod在dist目录下生成以下页面,虽然生成了两层目录,但是还是映射到’/mobile/home.html’, ‘/mobile/doctor_team.html’, ‘/mobile/about.html’查看打包以后的页面,body内容已经渲染好了启动http-server访问http://127.0.0.1:8080/mobile/about.html,效果跟正常访问一致原理prerender-spa-plugin 利用了 Puppeteer 的爬取页面的功能。 Puppeteer 是一个 Chrome官方出品的 headlessChromenode 库。它提供了一系列的 API, 可以在无 UI 的情况下调用 Chrome 的功能, 适用于爬虫、自动化处理等各种场景。它很强大,所以很简单就能将运行时的 HTML 打包到文件中。原理是在 Webpack 构建阶段的最后,在本地启动一个 Puppeteer 的服务,访问配置了预渲染的路由,然后将 Puppeteer 中渲染的页面输出到 HTML 文件中,并建立路由对应的目录。图片描述(最多50字)具体代码可以结合render-puppeteer下的代码来看图片描述(最多50字)在render.js中,启动本地服务,通过page.goto依次访问http://localhost:8000/mobile/about.html,http://localhost:8000/mobile/home.htmlhttp://localhost:8000/mobile/doctor_team.html通过page.content()获取html总结结合项目实践了下这个插件,也有不少坑1.在config/index.js中assetsPublicPath原先写的是/dist,导致预渲染的页面body没有渲染出来,是空白页面,改成/就能预渲染出来,但是这样导致不需要预渲染的页面资源路径不对2.该插件在webpack此版本下不支持路由懒加载,“webpack”: “^4.6.0”,网上提到npm i webpack@4.28.4可以解决,果然升级了webpack版本后支持路由懒加载3.在需要请求动态数据的页面中,预渲染只能保证静态部分不更改,如果不想写死,要做动态数据代理,webpack的devserver代理数据无效,需要用nginx或者其他代理工具代理线上数据 ...

February 17, 2019 · 1 min · jiezi

Webpack学习-工作原理(上)

对于webpack的认识始终停留在对脚手架的使用,不得不说脚手架既方便又好用,修改起来也方便,只需要知道webpack中各个配置项的功能,于是对于我们来说,webpack始终就是一个黑盒子,我们完全不清楚里面是如何去运作的。打包时报错,就只能借助google来协助帮忙解决问题,至于为什么要这样解决,什么原理,不管,能解决就好。那么,了解一下基本原理也是有必要。概念言归正传,我们一起了解一下webpack运行基本原理,首先先明白几个核心概念,Entry:入口,webpack构建的起始Module:模块,webpack里面一切皆模块,也是代表着文件,从Entry配置的入口文件开始,递归找出依赖的模块Chunk:代码块,找出递归依赖模块经转换后组合成代码块Loader:模块转换器,也就是将模块的内容按照需求装换成新内容Plugin:扩展插件,webpack构建过程中,会在特定的时机广播对应的事件,而插件可以监听这些事件的发生流程webpack构建流程,详细过程如下:初始化:从配置文件或是shell读取与合并参数,得到最终参数,实例化插件new Plugin()开始编译:通过上一步初始化得到的最终参数,初始化一个Compiler对象,加载插件(依次调用插件中的apply方法),通过执行Compiler.run开始编译确定入口:根据配置中entry找出所有入口文件编译模块:从entry出发,调用配置的loader,对模块进行转换,同时找出模块依赖的模块(如何找?见下文),依次递归,直到所有依赖模块完成本步骤处理完成模块编译:这一步已经使用loader对所有模块进行了转换,得到了转换后的新内容以及依赖关系输出资源: 根据入口与模块之间的依赖关系,组装成一个个chunk代码块,并且生成文件输出列表输出成功:根据配置中的输出路径和文件名,将文件写入文件系统,完成构建事件整个构建流程会发生很多的事件,来供Plugin监听,这些事件具体的可以分为三个阶段,分别是初始化阶段、编译阶段、输出阶段,那么具体有哪些事件,这里按阶段分别介绍,初始化阶段事件作用初始化从配置文件或是shell读取与合并参数,得到最终参数,依次实例化插件new Plugin()实例化Compiler通过上一步初始化得到的最终参数,初始化一个Compiler对象,负责监听文件和启动编译,全局只有一个Compiler对象加载插件依次调用插件中的apply方法,同时也会将Compiler实例传入,就可以调用Webpack提供的api,Compiler实例可以说是就是Webpack的实例environment将node.js风格的文件系统应用到compiler对象,便可以直接通过compiler来对文件进行操作entry-option读取配置中的entry,依次实例化出对应EntryPlugin,为后面该entry的递归解析工作做准备after-plugins调完所有内置和配置的插件的apply方法after-resolvers根据配置初始化resolvers,resolvers负责在文件系统中寻找制定路径的文件编译阶段事件作用run启动一次新的编译,调用Compiler.run()watch-run和run类似,区别在于它是在监听模式下进行编译的,这个事件可以获取哪些文件发生了变化从而导致新的一次编译compile告诉插件新的一次编译即将启动,并且给插件带上compiler对象compilation每当检测到文件的变化,都会有一次新的compilation被创建,一个compilation对象包含了当前的模块资源、编译生成的资源、变化的文件等等的属性和方法,同时记住,在很多事件的的回调中都会将compilation传入,以便使用make一个新的Compilation创建完毕,那么就会从entry配置中开始读取文件,使用配置好的loader对文件进行编译,编译完后再找出文件依赖的文件,递归地去编译和解析after-compile一次Compilation执行完成invalid文件编译错误等异常触发该事件,不会导致webpack退出Compilation的事件事件作用build-moudle使用对应的loader去转换一个模块normal-module-loader在用loader转换一个模块后,会使用acorn解析转换后的内容输出对应的抽象语法树(ast),以便webpack后面分析代码使用program从配置的入口开始,分析生成的ast,遇到require等导入语句时,便会将其加入依赖模块列表,并且对找出的依赖进行递归分析,最终可以弄清所有依赖关系seal所有模块及其依赖的模块都通过Loader转换完成,根据依赖关系生成chunk输出阶段事件作用should-emit所有需要输出的文件都生成,准备输出,询问哪些文件需要输出,哪些不需要输出emit确定好要输出哪些文件后,并执行文件输出,可以在这里获取和修改输出的内容after-emit文件输出完毕done完成一次完整的编译和输出流程failed编译和输出过程中运到异常,导致webpack退出,会直接到这个步骤,可以在这里获取具体原因总结Webpack是很好的前端资源加载和打包工具,在webpack里一切皆模块,很好地处理文件之间的依赖关系,这里我们介绍的是些理论性的知识,了解基本概念,知道整个流程是怎么样的,webpack是串行流水线运行的,工作期间会有很多广播事件,来供插件使用,这里我们介绍了各个阶段的事件以及作用,具体代码表示形式,后续文章会引入。

February 17, 2019 · 1 min · jiezi

记一次vue-webpack项目优化实践

项目现状项目是一个数据监测平台,引入了ehcart和three.js 负责项目的数据可视化;打包后,体积高达2.1M,这个体积相比于我的项目规模来说就显得稍有笨重了使用webpack-bundle-analyzer分析了一下各个文件所占用的比例:整个项目文件分布大体清晰了,现在开始优化走起!优化思路根据 wba的显示,第三方插件是大部头,包括three.js echart组件和elementUI组件。three.js优化空间不大,主要关注另外两个上面。echarts根据我的项目需求,echart主要用到的是linechart,其他图表不需要。而在开发过程中,我把整个echart都引用进来,其实是很没有必要的。ehcart整体引用方式import echarts from (“echarts”)vue.prototype.$echarts = echarts更改为:import echarts from “echarts/lib/echarts.js"import “echarts/lib/chart/line"import ’echarts/lib/component/tooltip’import ’echarts/lib/component/title’import ’echarts/lib/component/legend’import ’echarts/lib/component/legendScroll’import “echarts/lib/component/dataZoom"Vue.prototype.$echarts = echartselementUI同理echart,elementUI同样按需求导入,替换之前的整体引入。elementUI按需引入需要安装 babel-plugin-component包,在babelrc文件中进行如下修改:“plugins”: [ … [“component”, { “libraryName”: “element-ui”, “styleLibraryName”: “theme-chalk” }] ]优化后:经过对第三方插件的优化,打包后的文件缩小了近30%。目前为止,项目打包后的大部头就是three.js,这个目前的优化空间较小。而对echart改造给打包体积上带来的收益还是很明显的。后记这次的优化比较简单,主要是通过对自己项目的优化,熟悉webpack-bundle-analyzer的操作和使用这个插件的来优化webpack打包文件的方法和思路;算是简单的练手记录一下吧。当然,从整体优化的大维度上来说优化的点还有很多,这个文章继续更新下去。

February 15, 2019 · 1 min · jiezi

vue-cli3环境变量与分环境打包

第一步 : 了解环境变量概念我们可以根目录中的下列文件来指定环境变量:.env # 在所有的环境中被载入.env.local # 在所有的环境中被载入,但会被 git 忽略.env.[mode] # 只在指定的模式中被载入.env.[mode].local # 只在指定的模式中被载入,但会被 git 忽略环境变量文件只包含环境变量的“键=值”对:FOO=barVUE_APP_SECRET=secret // 只有VUE_APP_开头的环境变量可以在项目代码中直接使用除了 自定义的VUE_APP_* 变量之外,在你的应用代码中始终可用的还有两个特殊的变量:NODE_ENV - 会是 “development”、“production” 或 “test” 中的一个。具体的值取决于应用运行的模式。BASE_URL - 会和 vue.config.js 中的 publicPath 选项相符,即你的应用会部署到的基础路径。为一个特定模式准备的环境文件的 (例如 .env.production) 将会比一般的环境文件 (例如 .env) 拥有更高的优先级。模式概念: 模式是 Vue CLI 项目中一个重要的概念。一般情况下 Vue CLI 项目有三个默认模式:development 模式用于 vue-cli-service serveproduction 模式用于 vue-cli-service build 和 vue-cli-service test:e2etest 模式用于 vue-cli-service test:unit模式不等同于 NODE_ENV,一个模式可以包含多个环境变量。也就是说,每个模式都将 NODE_ENV的值设置为模式的名称(可重新赋值更改)——比如在 development 模式下 NODE_ENV 的值会被设置为 “development”。 你可以通过为 .env 文件增加后缀来设置某个模式下特有的环境变量。比如,如果你在项目根目录创建一个名为 .env.development 的文件,那么在这个文件里声明过的变量就只会在 development 模式下被载入。你可以通过传递 –mode 选项参数为命令行覆写默认的模式。例如,如果你想要在构建命令中使用开发环境变量,请在你的 package.json 脚本中加入:“dev-build”: “vue-cli-service build –mode development”,环境变量的使用 : 只有以 VUE_APP_ 开头的变量会被 webpack.DefinePlugin 静态嵌入到客户端侧的包中(即在项目代码中使用)。你可以在应用的代码中这样访问它们:console.log(process.env.VUE_APP_SECRET)理解指令 , 模式 , 环境变量之间的关系我们在项目中的package.json经常能看见以下这样的指令在一个 Vue CLI 项目中,@vue/cli-service 安装了一个名为 vue-cli-service 的命令。你可以在 npm scripts 中以 vue-cli-service、或者从终端中以 ./node_modules/.bin/vue-cli-service 访问这个命令。vue-cli-service serve用法:vue-cli-service serve [options] [entry]选项: –open 在服务器启动时打开浏览器 –copy 在服务器启动时将 URL 复制到剪切版 –mode 指定环境模式 (默认值:development) –host 指定 host (默认值:0.0.0.0) –port 指定 port (默认值:8080) –https 使用 https (默认值:false)vue-cli-service build用法:vue-cli-service build [options] [entry|pattern]选项: –mode 指定环境模式 (默认值:production) –dest 指定输出目录 (默认值:dist) –modern 面向现代浏览器带自动回退地构建应用 –target app | lib | wc | wc-async (默认值:app) –name 库或 Web Components 模式下的名字 (默认值:package.json 中的 “name” 字段或入口文件名) –no-clean 在构建项目之前不清除目标目录 –report 生成 report.html 以帮助分析包内容 –report-json 生成 report.json 以帮助分析包内容 –watch 监听文件变化以上是两个常用的cli指令 , 他们默认对应的分别是development和production模式 , 如果还想了解其他指令 , 可以访问: https://cli.vuejs.org/zh/guid… CLI 服务那么接下来 , 我们就开始创建一个用于打包测试环境的模式;修改package.json添加一行命令"test": “vue-cli-service build –mode test"添加.env.test文件在项目根路径创建.env.test文件,内容为NODE_ENV=‘production’ //表明这是生产环境(需要打包)VUE_APP_CURRENTMODE=‘test’ // 表明生产环境模式信息VUE_APP_BASEURL=‘http://.*.com:8000’ // 测试服务器地址修改项目中的api接口文件在我的项目中,一般会创建一个api.js 来管理所有的接口url因为我们在本地开发环境中是通过代理来连接服务器的,所以将url写成这${baseUrl}/apis/v1/login,在文件开头通过环境变量改变baseUrllet baseUrl = ‘’;if (process.env.NODE_ENV == ‘development’) { baseUrl = "” } else if (process.env.NODE_ENV == ‘production’) { baseUrl = process.env.VUE_APP_BASEURL} else { baseUrl = "" }当需要为测试环境进行打包的时候 , 我们只需要运行下面指令进行打包npm run test ...

February 15, 2019 · 2 min · jiezi

webpack4.0学习笔记

webpack基础webpack基础配置webpack打包出的文件解析Html插件样式处理转化es6语法处理js语法及校验全局变量引入问题图片处理打包文件分类webpack配置打包多页应用配置source-mapwatch的用法webpack小插件应用webpack跨域问题resolve属性的配置定义环境变量区分不同环境webpack性能优化noParseIgnorePlugindllPluginhappypackwebpack自带优化抽离公共代码懒加载热更新tapabletapable介绍tapableAsyncParralleHookAsyncSeriesHookAsyncSeriesWaterfall手写webpackwebpack手写webpack分析及处理创建依赖关系AST递归解析生成打包结果增加loader增加plugins手写loaderloaderloader配置babel-loader实现banner-loader实现实现file-loader和url-loaderless-loader和css-loadercss-loader手写pluginswebpack流程介绍webpack中的插件文件列表插件内联webpack插件打包后自动发布

February 15, 2019 · 1 min · jiezi

前端性能优化之gzip

gzip是GNUzip的缩写,它是一个GNU自由软件的文件压缩程序。它最早由Jean-loup Gailly和Mark Adler创建,用于UNⅨ系统的文件压缩。我们在Linux中经常会用到后缀为.gz的文件,它们就是GZIP格式的。现今已经成为Internet 上使用非常普遍的一种数据压缩格式,或者说一种文件格式。HTTP协议上的GZIP编码是一种用来改进WEB应用程序性能的技术。大流量的WEB站点常常使用GZIP压缩技术来让用户感受更快的速度。减少文件大小有两个明显的好处,一是可以减少存储空间,二是通过网络传输文件时,可以减少传输的时间。当然WEB服务器和客户端(浏览器)必须共同支持gzip。目前主流的浏览器Chrome,firefox,IE等都支持该协议。常见的服务器如Apache,Nginx,IIS同样支持gzip。下面就以Vue项目为例,介绍一下gzip的使用(vue-cli 2.*)1、在/config/index.js中,修改配置开启gzip// Gzip off by default as many popular static hosts such as// Surge or Netlify already gzip all static assets for you.// Before setting to true, make sure to:// npm install –save-dev compression-webpack-pluginproductionGzip: true,productionGzipExtensions: [‘js’, ‘css’],在修改productionGzip的默认值(false)为true之前,先安装所需的依赖npm install –save-dev compression-webpack-plugin。2、在nginx中开启gzip,/nginx/conf/nginx.conf中添加gzip配置http:{ #启用或禁用gzipping响应。# gzip on; #设置用于压缩响应的缓冲区number和size。默认情况下,缓冲区大小等于一个内存页面。这是4K或8K,具体取决于平台。# gzip_static on; #启用或禁用gzipping响应。# gzip_buffers 4 16k; #设置level响应的gzip压缩。可接受的值范围为1到9。# gzip_comp_level 5; #设置将被gzip压缩的响应的最小长度。长度仅由“Content-Length”响应头字段确定。# gzip_min_length 100; #匹配MIME类型进行压缩,text/html默认被压缩。# gzip_types text/plain application/javascript text/css application/xml text/javascript application/x-httpd-php image/jpeg image/gif image/png;}修改完nginx配置,重启服务。关于gzip详细的配置和描述,请查阅 Module ngx_http_gzip_module至此,gzip已开启。你可以运行你的项目去检测一下。打开Chrome控制台,可以看到Network下的Response Headers中返回了Content-Encoding: gzip,表明gzip开启成功。而Request Headers里面的Accept-Encoding: gzip只是表示前端(用户浏览器)支持gzip的压缩方式。服务器支持gzip的方式可以有两种: 1、打包的时候生成对应的.gz文件,浏览器请求xx.js时,服务器返回对应的xxx.js.gz文件 2、浏览器请求xx.js时,服务器对xx.js进行gzip压缩后传输给浏览器 ...

February 13, 2019 · 1 min · jiezi

webpack—url-loader 解决项目中图片打包路径问题

刚开始用webpack的同学很容易掉进图片打包这个坑里,比如打包出来的图片地址不对或者有的图片并不能打包进我们的目标文件夹里。下面我们就来分析下在webpack项目中图片的应用场景。1.CSS文件中的背景图等设置项目目录图:以下以我项目中的test.css为例.test{ color: red; width: 150px; height: 100px; overflow: hidden; background: url("../img/box@2x.png") no-repeat; background-size: 150px auto;}.img-base64{ color: red; width: 150px; height: 100px; overflow: hidden; background: url("../img/media1.png") no-repeat; background-size: 100px auto;}#img-e { width:100px;}2.html文件中引入图片,下面为模板文件index.html<!DOCTYPE html><html lang=“en”><head> <meta charset=“UTF-8”> <title>webpack-img</title></head><body>1.css背景图 图片格式<div class=“test”></div>2.css背景图 转化为base64<div class=“img-base64”></div>3.html里引入图片<div><img src=“img/media4.png” alt=""></div>4.js里引入图片</body></html>3.js中引入图片require(’../css/test.css’);var imgSrc = require(’../img/do.gif’);var img = new Image();img.id = ‘img-e’;img.src = imgSrc;document.body.appendChild(img);url-loader在 webpack 中引入图片需要依赖 url-loader 这个加载器。在 webpack.config.js 文件中配置如下:{ test:/.(jpg|png|gif|bmp|jpeg)$/, loader: ‘url-loader?limit=8192&name=img/[hash:8].[name].[ext]’ }test 属性代表可以匹配的图片类型,除了 png、jpg 之外也可以添加 gif 等,以竖线隔开即开。loader 后面 limit 字段代表图片打包限制,这个限制并不是说超过了就不能打包,而是指当图片大小小于限制时会自动转成 base64 码引用。上例中大于8192字节的图片正常打包,小于8192字节的图片以 base64 的方式引用。url-loader 后面除了 limit 字段,还可以通过 name 字段来指定图片打包的目录与文件名而使用extractTextPlugin插件时,需要配置publicPath: “../”, 不配置时css文件中背景图默认地址会在css文件夹下查找图片资源,导致项目图片路径不正确webpack.config.js 配置文件如下:const webpack = require(“webpack”)const HtmlWebpackPlugin = require(‘html-webpack-plugin’)const extractTextPlugin = require(“extract-text-webpack-plugin”)const path = require(‘path’)const extractCSS = new extractTextPlugin(“css/[name].[hash:6].css”)//const OptimizeCssAssetsPlugin = require(‘optimize-css-assets-webpack-plugin’);//压缩css插件module.exports = { //注意这里是exports不是 entry: ‘./src/js/main.js’, output: { publicPath:"./", path: path.resolve(__dirname + “/dist”), //打包后的js文件存放的地方 filename: “js/[name].[hash:6].js” //打包后的js文件名 }, plugins: [ extractCSS, new OptimizeCssAssetsPlugin(),//压缩css new webpack.optimize.UglifyJsPlugin(),//new uglify(),//压缩js new HtmlWebpackPlugin({ filename: ‘index.html’, template:‘src/index.html’ }) ], module: { rules: [ //1.0的是loaders //处理js中的loader { test: /.js$/, loader: ‘babel-loader’, include: path.resolve(__dirname, ‘/src’), //指定打包的文件 exclude: path.resolve(__dirname, ‘/node_modules’) //排除打包的文件,加速打包时间 }, //处理css中的loader { test: /.css$/, use: extractTextPlugin.extract({ fallback: “style-loader”, publicPath: “../”, use: [ { loader: ‘css-loader’, options:{ minimize: true //css压缩 } } ] }) }, //处理html模板中的loader { test: /.html$/, loader: ‘html-loader’ }, //处理ejs模板中的loader,以.tpl后缀结尾的 { test: /.tpl$/, loader: ’ejs-loader’ }, //处理图片中的loader,file-loader,url-loader,image-webpack-loader相互配合(图片格式转换base64 图片压缩) { test:/.(jpg|png|gif|bmp|jpeg)$/, loader: ‘url-loader?limit=8192&name=img/[hash:8].[name].[ext]’ } ] }}; ...

February 13, 2019 · 1 min · jiezi

08_00_构建

In this part, you enable source maps on the build, discuss how to split it into separate bundles in various ways, and learn to tidy up the result.本章中,在构建中启用了源码映射,讨论了用不同方法将其分离到不同的Bundle中,以及学习清理结果。

February 4, 2019 · 1 min · jiezi

vue入门(一):项目搭建

前言我的JS水平比较一般,而且还是跨专业半路出家,因此学习是唯一出路。vue并不是我接触的第一个前端框架,之前学习过angular1.x,觉得不太容易,结果没多久2版本就推出了,一看文档:totally rewrite。WTF???1还没学利索呢,2就重写了?从此抛弃angular。直到后来,公司需要做个管理后台系统,经过一番比较最终选择了vue,原因:angular已拉黑react里的jsx语法一时不容易掌握vue学习成本较低,简单易上手,性能也很优秀二话不说立马上手,我之前的项目都是通过vue-cli创建的,而其中的webpack配置并不特别贴合项目中的要求,由于我之前已经写了webpack系列的博文,所以在这里就从0-1搭建一个vue项目。1. 开始1.1 安装npm install vue vue-router -S在项目中我们使用 .vue 文件进行开发,所以还要安装一些工具:npm install vue-loader vue-style-loader vue-template-compiler -D1.2 整理目录打开我们之前的webpack项目,删除 src 目录下的所有文件,并在其下创建 asset 文件夹,用来放置资源文件;pages 文件夹,来放置我们的页面文件;router 文件夹,路由配置;http 文件夹,ajax请求配置;app.js 入口文件,还有一个 app.vue 文件,如图所示:然后就可以写代码啦………2. 下面正式写代码2.1 认识 .vue 文件这个 .vue 文件是啥呢,官方文档大概是这么说的:在很多 Vue 项目中,我们使用 Vue.component 来定义全局组件,紧接着用 new Vue({ el: ‘#container ‘}) 在每个页面内指定一个容器元素。这种方式在很多中小规模的项目中运作的很好,在这些项目里 JavaScript 只被用来加强特定的视图。但当在更复杂的项目中,或者你的前端完全由 JavaScript 驱动的时候,下面这些缺点将变得非常明显:全局定义 (Global definitions) 强制要求每个 component 中的命名不得重复字符串模板 (String templates) 缺乏语法高亮不支持 CSS (No CSS support) 意味着当 HTML 和 JavaScript 组件化时,CSS 明显被遗漏没有构建步骤 (No build step) 限制只能使用 HTML 和 ES5 JavaScript, 而不能使用预处理器,如 Babel文件扩展名为 .vue 的 single-file components(单文件组件) 为以上所有问题提供了解决方法,并且还可以使用 webpack等构建工具。这是一个文件名为 Hello.vue 的简单实例:编写 app.vue 文件:<template> <div> <h1 class=“red”>{{text }} this is main</h1> </div></template><script>export default { data () { return { text: ‘hello world’ } }, mounted () { console.log(‘app is mounted’) }}</script><style> .red { color: red; }</style>编写入口文件 app.js:import Vue from ‘vue’import App from ‘./app.vue’new Vue({ el: ‘#app’, render: function (h) { return h(App) }})2.2 配置webpack在 config 目录下创建 vue-loader.config.js :// vue-loader.config.js module.exports = function (isDev) { return { preserveWhiteSpace: true, extractCss: !isDev, cssModules: { localIdentName: isDev ? ‘[path]-[name]-[hash:base64:5]’ : ‘[hash:base64:5]’, camelCase: true }, hotReload: isDev //根据环境变量生成 }}修改 webpack.config.js :// 引入vue-loader.configconst createVueLoaderOptions = require(’./vue-loader.config’)// 引入VueLoaderPluginconst VueLoaderPlugin = require(‘vue-loader/lib/plugin’)// 修改入口entry: { app: path.join(__dirname, ‘../src/app.js’) }// 修改loaders配置{ test: /.vue$/, loader: ‘vue-loader’, options: createVueLoaderOptions(isDev) }, { test: /.css$/, use: [ isDev ? ‘vue-style-loader’ : MiniCssExtractPlugin.loader, { loader: ‘css-loader’, options: { importLoaders: 1 } }, ‘postcss-loader’ ] }, { test: /.less$/, use: [ isDev ? ‘vue-style-loader’ : MiniCssExtractPlugin.loader, ‘css-loader’, { loader: ‘postcss-loader’, options: { sourceMap: true } }, ’less-loader’ ] }// 添加VueLoaderPluginnew VueLoaderPlugin(),// 修改HtmlWebpackPluginnew HtmlWebpackPlugin({ template: path.join(__dirname, ‘../app.html’), inject: true, minify: { removeComments: true } })至此,所有配置完毕,执行npm run dev如果配置没错,项目就成功跑起来了 ...

February 4, 2019 · 2 min · jiezi

源码映射

When your source code has gone through transformations, debugging becomes a problem. When debugging in a browser, how to tell where the original code is? Source maps solve this problem by providing a mapping between the original and the transformed source code. In addition to source compiling to JavaScript, this works for styling as well.当你的源代码完成转换后,调试就成了一个问题。在浏览器中调试时,如何确认代码中的原始位置呢?Source maps 通过建立源码与转换后的代码之间的映射关系来解决这个问题。除了可以用在编译后的Javascript中,还可以用在样式中。One approach is to skip source maps during development and rely on browser support of language features. If you use ES2015 without any extensions and develop using a modern browser, this can work. The advantage of doing this is that you avoid all the problems related to source maps while gaining better performance.在开发中不使用源码映射的方法是依赖于浏览器对语言特性的支持。如果你只使用ES015而不使用任何扩展并且在开发中使用现代浏览器,这是可以实现的。这样做的好处就是为了获取更好的性能时可以避免由于源码映射导致的性能下降问题。If you are using webpack 4 and the new mode option, the tool will generate source maps automatically for you in development mode. Production usage requires attention, though.如果你在使用Webpack4并且使用 mode 选项,在development模式下,Webpack会自动生成源码映射关系。但是需要注意生产环境的使用。T> If you want to understand the ideas behind source maps in greater detail, read Ryan Seddon’s introduction to the topic.如果想深入了解source map的机制,可以读Ryan Seddon的介绍.T> To see how webpack handles source maps, see source-map-visualization by the author of the tool.T> 想了解webpack如何处理源码映射, 查看工具作者写的source-map-visualization .Inline Source Maps and Separate Source Maps(内联源码映射和分离源码映射)Webpack can generate both inline or separate source map files. The inline ones are valuable during development due to better performance while the separate ones are handy for production use as it keeps the bundle size small. In this case, loading source maps is optional.Webpack即可以生成内联源码映射也可以生成分离源码映射。内联源码映射在开发时非常有用,而为了追求生产环境中更好的性能,分离源码映射通过保持Bundle的文件更小而更加方便。这种情况下,加载源码映射是选择性的。It’s possible you don’t want to generate a source map for your production bundle as this makes it effortless to inspect your application. By disabling source maps, you are performing a sort of obfuscation. Whether or not you want to enable source maps for production, they are handy for staging. Skipping source maps speeds up your build as generating source maps at the best quality can be a complicated operation.你一定不想在生产环境中生成源码映射,因为它能用来非常容易的分析你的程序。通过禁用源码映射,你要执行一系列混淆。无论你想不想启用源码映射,但是它们对于演示来说非常方便。跳过源码映射可以加快你的构建速度,而生成源码映射可以提升质量,这可能成为一个复杂的操作。Hidden source maps give stack trace information only. You can connect them with a monitoring service to get traces as the application crashes allowing you to fix the problematic situations. While this isn’t ideal, it’s better to know about possible problems than not.隐藏源码映射 只能提供堆栈跟踪信息。在程序崩溃的时候,可以让你把他们连接到监视服务来获取跟踪信息来解决有问题的情况。虽然这样并不理想,但是知道可能的问题总比不知道要好。T> It’s a good idea to study the documentation of the loaders you are using to see loader specific tips. For example, with TypeScript, you have to set a particular flag to make it work as you expect.学习你在使用的加载器的具体提示是个好主意。例如,在Typescript中,你得设置一个标志才能让它按照预想的方式工作。Enabling Source Maps(启用源码映射)Webpack provides two ways to enable source maps. There’s a devtool shortcut field. You can also find two plugins that give more options to tweak. The plugins are going to be discussed briefly at the end of this chapter. Beyond webpack, you also have to enable support for source maps at the browsers you are using for development.Webpack有两种方式来启用源码映射。有一个字段devtool。你也能找到两个插件,他们能提供更多的选项做进一步配置。这些插件将在本文最后做个简单的介绍。除了Webpack,我们还得在开发使用的浏览器中启用源码映射支持。Enabling Source Maps in Webpack(启用Webpack中的源码映射)To get started, you can wrap the core idea within a configuration part. You can convert this to use the plugins later if you want:在开始前,你可以把这个核心思想封装在一个配置部分中。如果愿意,你可以稍后将其转换成插件:webpack.parts.jsexports.generateSourceMaps = ({ type }) => ({ devtool: type,});Webpack supports a wide variety of source map types. These vary based on quality and build speed. For now, you enable source-map for production and let webpack use the default for development. Set it up as follows:Webpack支持很多种源码映射类型。他们的不同在于质量和构建速度。现在,为生产环境启用source-map并且让webpack对开发环境使用默认配置。按如下方式配置:webpack.config.jsconst productionConfig = merge([leanpub-start-insert parts.generateSourceMaps({ type: “source-map” }),leanpub-end-insert …]);source-map is the slowest and highest quality option of them all, but that’s fine for a production build.source-map是最慢但是质量却最高的,这对生产环境构建很适合。{pagebreak}If you build the project now (npm run build), you should see source maps in the output:如果你现在构建项目(npm run build),就能在输出中看到源码映射:Hash: b59445cb2b9ae4cea11bVersion: webpack 4.1.1Time: 1347msBuilt at: 3/16/2018 4:58:14 PM Asset Size Chunks Chunk Names main.js 838 bytes 0 [emitted] main main.css 3.49 KiB 0 [emitted] main main.js.map 3.75 KiB 0 [emitted] mainmain.css.map 85 bytes 0 [emitted] main index.html 220 bytes [emitted]Entrypoint main = main.js main.css main.js.map main.css.map…Take a good look at those .map files. That’s where the mapping between the generated and the source happens. During development, it writes the mapping information in the bundle.仔细查看这些 .map 文件。这就是保存源码和编译后的代码的映射关系的地方。在开发的时候,它把映射信息直接写到Bundle中。Enabling Source Maps in Browsers(在浏览器中启用源码映射)To use source maps within a browser, you have to enable source maps explicitly as per browser-specific instructions:要想在浏览器中使用源码映射,你得根据不同浏览器的具体介绍准确启用源码映射。Chrome. Sometimes source maps will not update in Chrome inspector. For now, the temporary fix is to force the inspector to reload itself by using alt-r.FirefoxIE EdgeSafariW> If you want to use breakpoints (i.e., a debugger; statement or ones set through the browser), the eval-based options won’t work in Chrome!如果你想用断点(例如,一个debugger语句或者通过浏览器设置),在chrome中基于eval的选项不起作用。Source Map Types Supported by Webpack(webpack支持的源码映射类型)Source map types supported by webpack can be split into two categories:Webpack支持的源码映射可以分为两类:Inline source maps add the mapping data directly to the generated files.Inline 源码映射直接将映射数据添加到生成文件中.Separate source maps emit the mapping data to separate source map files and link the source to them using a comment. Hidden source maps omit the comment on purpose.Separate 源码映射将映射数据生成到独立的源码映射文件中并通过注释将他们的源码关联起来. Hidden source maps故意省略注释。Thanks to their speed, inline source maps are ideal for development. Given they make the bundles big, separate source maps are the preferred solution for production. Separate source maps work during development as well if the performance overhead is acceptable.内联源码映射由于速度快非常适合开发使用。由于他们使Bundle变大,分离的源码映射更受到生产环境的青睐。如果性能开销可以接受,分离源码映射也可以用在开发中。Inline Source Map Types(内联源码映射类型)Webpack provides multiple inline source map variants. Often eval is the starting point and webpack issue #2145 recommends cheap-module-eval-source-map as it’s a good compromise between speed and quality while working reliably in Chrome and Firefox browsers.Webpack提供多种内联源码映射变体。eval通常作为开始点,在webpack issue #2145中推荐使用cheap-module-eval-source-map由于其很好的平衡了速度和质量并且在Firefox和Chrome中运行稳定。To get a better idea of the available options, they are listed below while providing a small example for each. The source code contains only a single console.log(‘Hello world’) and webpack.NamedModulesPlugin is used to keep the output easier to understand. In practice, you would see a lot more code to handle the mapping.为了更好的离解可用的选项,下面列举并使用短小的例子进行说明。源码中只包含一句console.log(‘Hello world’) 并且 webpack.NamedModulesPlugin保证输出更易于理解 。在实际中,你将会看到更多的代码来处理映射关系。devtool: “eval"eval generates code in which each module is wrapped within an eval function:eval生成代码,每个模块都封装在一个eval函数中:webpackJsonp([1, 2], { “./src/index.js”: function(module, exports) { eval(“console.log(‘Hello world’);\n\n//////////////////\n// WEBPACK FOOTER\n// ./src/index.js\n// module id = ./src/index.js\n// module chunks = 1\n\n//# sourceURL=webpack:///./src/index.js?”) }}, [”./src/index.js"]);devtool: “cheap-eval-source-map"cheap-eval-source-map goes a step further and it includes base64 encoded version of the code as a data url. The result contains only line data while losing column mappings.cheap-eval-source-map更进一步,它包含base64编码版本的代码作为数据的URL。结果中只包含行数据而没有列的映射数据。webpackJsonp([1, 2], { “./src/index.js”: function(module, exports) { eval(“console.log(‘Hello world’);//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiLi9hcHAvaW5kZXguanMuanMiLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly8vLi9hcHAvaW5kZXguanM/MGUwNCJdLCJzb3VyY2VzQ29udGVudCI6WyJjb25zb2xlLmxvZygnSGVsbG8gd29ybGQnKTtcblxuXG4vLy8vLy8vLy8vLy8vLy8vLy9cbi8vIFdFQlBBQ0sgRk9PVEVSXG4vLyAuL2FwcC9pbmRleC5qc1xuLy8gbW9kdWxlIGlkID0gLi9hcHAvaW5kZXguanNcbi8vIG1vZHVsZSBjaHVua3MgPSAxIl0sIm1hcHBpbmdzIjoiQUFBQSIsInNvdXJjZVJvb3QiOiIifQ==”) }}, [”./src/index.js"]);{pagebreak}If you decode that base64 string, you get output containing the mapping:如果解码base64的字符串,就能得到包含映射关系的输出:{ “file”: “./src/index.js”, “mappings”: “AAAA”, “sourceRoot”: “”, “sources”: [ “webpack:///./src/index.js?0e04” ], “sourcesContent”: [ “console.log(‘Hello world’);\n\n\n//////////////////\n// WEBPACK FOOTER\n// ./src/index.js\n// module id = ./src/index.js\n// module chunks = 1” ], “version”: 3}devtool: “cheap-module-eval-source-map"cheap-module-eval-source-map is the same idea, except with higher quality and lower performance:cheap-module-eval-source-map 也是相同的思路,只是质量更高,但是性能更低:webpackJsonp([1, 2], { “./src/index.js”: function(module, exports) { eval(“console.log(‘Hello world’);//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiLi9hcHAvaW5kZXguanMuanMiLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly8vYXBwL2luZGV4LmpzPzIwMTgiXSwic291cmNlc0NvbnRlbnQiOlsiY29uc29sZS5sb2coJ0hlbGxvIHdvcmxkJyk7XG5cblxuLy8gV0VCUEFDSyBGT09URVIgLy9cbi8vIGFwcC9pbmRleC5qcyJdLCJtYXBwaW5ncyI6IkFBQUEiLCJzb3VyY2VSb290IjoiIn0=”) }}, [”./src/index.js"]);{pagebreak}Again, decoding the data reveals more:再一次,解码数据揭示更多信息:{ “file”: “./src/index.js”, “mappings”: “AAAA”, “sourceRoot”: “”, “sources”: [ “webpack:///src/index.js?2018” ], “sourcesContent”: [ “console.log(‘Hello world’);\n\n\n// WEBPACK FOOTER //\n// src/index.js” ], “version”: 3}In this particular case, the difference between the options is minimal.在这个特定的情况中,选项之间的差异是很小的。devtool: “eval-source-map"eval-source-map is the highest quality option of the inline options. It’s also the slowest one as it emits the most data:eval-source-map 是内联选项中质量最高的选项。由于它产生最多的数据所以也是最慢的。webpackJsonp([1, 2], { “./src/index.js”: function(module, exports) { eval(“console.log(‘Hello world’);//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly8vLi9hcHAvaW5kZXguanM/ZGFkYyJdLCJuYW1lcyI6WyJjb25zb2xlIiwibG9nIl0sIm1hcHBpbmdzIjoiQUFBQUEsUUFBUUMsR0FBUixDQUFZLGFBQVoiLCJmaWxlIjoiLi9hcHAvaW5kZXguanMuanMiLCJzb3VyY2VzQ29udGVudCI6WyJjb25zb2xlLmxvZygnSGVsbG8gd29ybGQnKTtcblxuXG4vLyBXRUJQQUNLIEZPT1RFUiAvL1xuLy8gLi9hcHAvaW5kZXguanMiXSwic291cmNlUm9vdCI6IiJ9”) }}, [”./src/index.js"]);{pagebreak}This time around there’s more mapping data available for the browser:这回有更多的映射数据可供浏览器使用:{ “file”: “./src/index.js”, “mappings”: “AAAAA,QAAQC,GAAR,CAAY,aAAZ”, “names”: [ “console”, “log” ], “sourceRoot”: “”, “sources”: [ “webpack:///./src/index.js?dadc” ], “sourcesContent”: [ “console.log(‘Hello world’);\n\n\n// WEBPACK FOOTER //\n// ./src/index.js” ], “version”: 3}Separate Source Map Types(分离式源码映射类型)Webpack can also generate production usage friendly source maps. These end up in separate files ending with .map extension and are loaded by the browser only when required. This way your users get good performance while it’s easier for you to debug the application.Webpack还能生成适用于生产环境的源码映射。它们最终存入以.map为扩展名的单独文件中,并且只在需要的情况下由浏览器加载。用户可以获得更好的性能,同时他也可以Debug程序。source-map is a reasonable default here. Even though it takes longer to generate the source maps this way, you get the best quality. If you don’t care about production source maps, you can skip the setting there and get better performance in return.source-map是这里理所当然的默认值。尽管按这种方式生成代码映射会耗费更长时间,但是你能获得更好的性能。如果你不介意生产环境的代码映射,可以跳过此步来获取更好的性能。{pagebreak}devtool: “cheap-source-map"cheap-source-map is similar to the cheap options above. The result is going to miss column mappings. Also, source maps from loaders, such as css-loader, are not going to be used.cheap-source-map和上面介绍的简易选项一样。结果就是丢失列映射关系。此外,来自加载器的源码映射(如css-loader)将不能使用。Examining the .map file reveals the following output in this case:在这个例子中,查看.map文件可以得到以下输出:{ “file”: “main.9aff3b1eced1f089ef18.js”, “mappings”: “AAAA”, “sourceRoot”: “”, “sources”: [ “webpack:///main.9aff3b1eced1f089ef18.js” ], “sourcesContent”: [ “webpackJsonp([1,2],{"./src/index.js":function(o,n){console.log("Hello world")}},["./src/index.js"]);\n\n\n// WEBPACK FOOTER //\n// main.9aff3b1eced1f089ef18.js” ], “version”: 3}The source contains //# sourceMappingURL=main.9a…18.js.map kind of comment at its end to map to this file.源码结尾中包含 //# sourceMappingURL=main.9a…18.js.map这样的注释来指向分离的映射文件。{pagebreak}devtool: “cheap-module-source-map"cheap-module-source-map is the same as previous except source maps from loaders are simplified to a single mapping per line. It yields the following output in this case:除了加载器的源映射被简化为每行一个映射之外,cheap-module-source-map与前面的映射是相同的。在这种情况下,它产生如下输出:{ “file”: “main.9aff3b1eced1f089ef18.js”, “mappings”: “AAAA”, “sourceRoot”: “”, “sources”: [ “webpack:///main.9aff3b1eced1f089ef18.js” ], “version”: 3}W> cheap-module-source-map is currently broken if minification is used and this is an excellent reason to avoid the option for now.W> 如果使用了minification cheap-module-source-map 就会遭到破坏,这也成为不使用这个选项的绝佳理由.devtool: “hidden-source-map"hidden-source-map is the same as source-map except it doesn’t write references to the source maps to the source files. If you don’t want to expose source maps to development tools directly while you wish proper stack traces, this is handy.hidden-source-map与source-map一样,只不过它不会将源码映射的引用写入源文件中。如果您不想直接将源映射公开给开发工具,而希望使用适当的堆栈跟踪,那么这是非常方便的。devtool: “nosources-source-map"nosources-source-map creates a source map without sourcesContent in it. You still get stack traces, though. The option is useful if you don’t want to expose your source code to the client.nosources-source-map创建的源码映射里不包含代码内容。但是你还是能得到堆栈跟踪信息。如果不想暴露源码给用户这个选项很有用。T> The official documentation contains more information about devtool options.T> The official documentation 包含更多关于 devtool 选项的信息.{pagebreak}devtool: “source-map"source-map provides the best quality with the complete result, but it’s also the slowest option. The output reflects this:source-map用完整的结果提供最好的质量,但是它也是最慢的选项。输出如下所示:{ “file”: “main.9aff3b1eced1f089ef18.js”, “mappings”: “AAAAA,cAAc,EAAE,IAEVC,iBACA,SAAUC,EAAQC,GCHxBC,QAAQC,IAAI,kBDST”, “names”: [ “webpackJsonp”, “./src/index.js”, “module”, “exports”, “console”, “log” ], “sourceRoot”: “”, “sources”: [ “webpack:///main.9aff3b1eced1f089ef18.js”, “webpack:///./src/index.js” ], “sourcesContent”: [ “webpackJsonp([1,2],{\n\n// "./src/index.js":\n// (function(module, exports) {\n\nconsole.log(‘Hello world’);\n\n/***/ })\n\n},["./src/index.js"]);\n\n\n// WEBPACK FOOTER //\n// main.9aff3b1eced1f089ef18.js”, “console.log(‘Hello world’);\n\n\n// WEBPACK FOOTER //\n// ./src/index.js” ], “version”: 3}{pagebreak}Other Source Map Options(其他源码映射选项)There are a couple of other options that affect source map generation:还有其他一些选项影响源码映射的生成:{ output: { // Modify the name of the generated source map file. // You can use [file], [id], and [hash] replacements here. // The default option is enough for most use cases. sourceMapFilename: ‘[file].map’, // Default // This is the source map filename template. It’s default // format depends on the devtool option used. You don’t // need to modify this often. devtoolModuleFilenameTemplate: ‘webpack:///[resource-path]?[loaders]’ },}T> The official documentation digs into output specifics.T> 官方文档 深入研究了output的细节 .W> If you are using UglifyJsPlugin and still want source maps, you need to enable sourceMap: true for the plugin. Otherwise, the result isn’t what you expect because UglifyJS will perform a further transformation of the code, breaking the mapping. The same has to be done with other plugins and loaders performing changes. css-loader and related loaders are a good example.W> 如果你在使用 UglifyJsPlugin,并且想使用源码映射,你需要在插件中启用sourcemap:true。否则,结果与你的预期会不一致,因为UglifyJS会执行更深入的代码转换从而破坏了映射。在其他的插件和加载器中也需要做相应的修改。css-loader和相关的加载器就是很好的例子。SourceMapDevToolPlugin and EvalSourceMapDevToolPluginIf you want more control over source map generation, it’s possible to use the SourceMapDevToolPlugin or EvalSourceMapDevToolPlugin instead. The latter is a more limited alternative, and as stated by its name, it’s handy for generating eval based source maps.如果想对源码映射进行更多的控制,你可以使用SourceMapDevToolPlugin 或者 EvalSourceMapDevToolPlugin 来代替. 后者是一种更有限的选择,正如其名称所述,它对于生成基于eval的源码映射非常方便。Both plugins can allow more granular control over which portions of the code you want to generate source maps for, while also having strict control over the result with SourceMapDevToolPlugin. Using either plugin allows you to skip the devtool option altogether.这两个插件都允许更细粒度地控制要为哪部分代码生成源码映射, 同时还可以使用“SourceMapDevToolPlugin”严格控制结果。使用任何一个插件都可以跳过“devtool”选项。Given webpack matches only .js and .css files by default for source maps, you can use SourceMapDevToolPlugin to overcome this issue. This can be achieved by passing a test pattern like /.(js|jsx|css)($|?)/i.考虑到webpack生成源码匹配时默认只匹配 .js 和 .css 文件, 你可以使用 SouceMapDevToolPlugin来解决这个问题。可以通过传入一个 像/.(js|jsx|css)($|?)/itest这样的test模式来解决。EvalSourceMapDevToolPlugin accepts only module and lineToLine options as described above. Therefore it can be considered as an alias to devtool: “eval” while allowing a notch more flexibility.如上所述,EvalSourceMapDevToolPlugin只接受module和lineToLine选项。因此可以把它看成是devtool: “eval"的别称,只不过是配置更富弹性。Changing Source Map Prefix(修改源码映射前缀)You can prefix a source map option with a pragma character that gets injected into the source map reference. Webpack uses # by default that is supported by modern browsers, so you don’t have to set it.您可以在源码映射选项前面加上一个编译指示字符,该字符被注入到源码映射引用中。Webpack默认使用现代浏览器都支持的#,所以不用特地去设置它。To override this, you have to prefix your source map option with it (e.g., @source-map). After the change, you should see //@ kind of reference to the source map over //# in your JavaScript files assuming a separate source map type was used.要替换它的话,你需要把它添加到源码映射选项的前面(例如:@source-map).修改之后,如果是正在使用分离映射文件,你应该能在源码映射中看到//@这样的引用而不是//#。Using Dependency Source Maps(使用依赖源码映射)Assuming you are using a package that uses inline source maps in its distribution, you can use source-map-loader to make webpack aware of them. Without setting it up against the package, you get minified debug output. Often you can skip this step as it’s a special case.假设你在使用一个包,它在使用内联源码映射,你可以使用source-map-loader来让Webpack注意到他们。如果没有对这个包进行设置,你会获得较少的输出。由于这是一个特例,通常你可以路过此步。Source Maps for Styling(样式的源码映射)If you want to enable source maps for styling files, you can achieve this by enabling the sourceMap option. The same idea works with style loaders such as css-loader, sass-loader, and less-loader.如果想在样式文件中使用源码映射,可以通过启用sourceMap选项来实现。相同的想法对样式加载器如 css-loader, sass-loader和 less-loader也适用。The css-loader is known to have issues when you are using relative paths in imports. To overcome this problem, you should set output.publicPath to resolve the server url.在imports中使用相对路径会导致 css-loader 的已知问题发生.要解决这个问题,你应该设置 output.publicPath来处理服务器路径。{pagebreak}Conclusion(总结)Source maps can be convenient during development. They provide better means to debug applications as you can still examine the original code over a generated one. They can be valuable even for production usage and allow you to debug issues while serving a client-friendly version of your application.源码映射给开发带来了便利。他们提供了调试应用程序更好的方法,因为你仍然可以通过生成的代码检查原始代码。他们在生产环境中使用也是很有意义并且让你可以在对用户友好的版本中调试问题。To recap:(概述)Source maps can be helpful both during development and production. They provide more accurate information about what’s going on and make it faster to debug possible problems.源码映射对于开发环境和生产环境都非常有帮助。Webpack supports a large variety of source map variants. They can be split into inline and separate source maps based on where they are generated. Inline source maps are handy during development due to their speed. Separate source maps work for production as then loading them becomes optional.Webpack 很多种源码映射变体。他们可以根据生成位置的不同被分成内联式的和分离式的源码映射。因为内联源码映射速度快非常适合开发阶段。由于源码映射加载是可选的,分离源码映射适合于生产环境.devtool: “source-map” is the highest quality option making it valuable for production.devtool: “source-map” 是最高质量的选项,因此非常适合生产环境.cheap-module-eval-source-map is a good starting point for development.cheap-module-eval-source-map 是开发的一个好起点。If you want to get only stack traces during production, use devtool: “hidden-source-map”. You can capture the output and send it to a third party service for you to examine. This way you can capture errors and fix them.如果只想在生产环境中获得堆栈跟踪,使用 devtool: “hidden-source-map。您可以捕获输出并将其发送到第三方服务以供检查。这样你 能捕获错误并修复他们。SourceMapDevToolPlugin and EvalSourceMapDevToolPlugin provide more control over the result than the devtool shortcut.SourceMapDevToolPlugin 和 EvalSourceMapDevToolPlugin 比 devtool 对结果提供更多的控制。source-map-loader can come in handy if your dependencies provide source maps.如果依赖包提供源码映射,那么用source-map-loader就会很方便。Enabling source maps for styling requires additional effort. You have to enable sourceMap option per styling related loader you are using.为式样启用源码映射需要进行额外的配置. 需要为每个加载器启用 sourceMap 选项。In the next chapter, you’ll learn to split bundles and separate the current one into application and vendor bundles.在下一节中,你将学到分离bundle并将现在的Bundle分离到应用Bundle和供应商Bundle中。 ...

February 4, 2019 · 11 min · jiezi

使用webpack + electron + reactJs开发windows桌面应用

electron是一两年前挺火的一个框架本质上是一个浏览器,但是集成了很多windows系统的功能,让前端开发也可以直接操作windows的窗体,做成一个实打实的桌面软件(当然听说mac上也可以用electron,不过没试过)(没错我还在用windows,不是mac也不是linux,我是个lowB)团队主要的技术栈是react,所以考虑用react开发,方便维护。PS.由于项目是大半年前做的,所以一些细节可能记忆有误请见谅几个重点:1.想要能调试必须使用webpack打包,不能用react那些常用的打包脚手架,因为webpack打包有target: “electron-main"2.对于不使用electron模块的项目,electron可以直接跑任何网页;对于用到electron模块的项目,如果不设置target: “electron-main”,而直接用webpack打包(或者其他的打包工具),打包工具会直接默认把electron模块一起打包进去。而electron模块里会用到node的比如fs模块,这些模块都不允许在网页上调用,因为需要直接访问电脑文件下面开始我们知道electron其实是有两个部分的,一个是窗体部分,一个是窗体里运行的网页项目窗体部分通常放在根目录下,只使用main.js一个文件来控制网页项目部分一般放在src目录下,打包出来的文件放到dist目录下目录大致如下main.js文件里会对窗体部分做很多配置具体可以参见electron的官方文档:electron官方文档mainWnd = new BrowserWindow({ // 窗体配置参数});mainWnd.loadURL(file://${__dirname}/dist/index.html); //这句话是用于配置窗体加载的网页项目的,配置为打包后的目录网页项目部分使用ipc模块与electron的窗体部分的ipcMain模块进行通信,网页项目部分可以发送以某个指令给窗体部分网页项目部分发送指令// src/MyComponent.jsconst ipc = require(’electron’).ipcRenderer;ipc.send(’logout’);窗体部分接收到指令后做相应的行为//main.jsipcMain.on(’logout’, function (event, arg) { // do something console.log(’logint’);});窗体部分也可以使用webContent模块与网页项目部分通信比如用户点击关闭窗体,可以使用event.preventDefault();阻塞关闭,然后通知网页项目部分,做退出登录的行为,退登完成之后再关闭窗体// main.js mainWnd.webContents.send(‘mainWnd-close’);网页项目部分做对应的行为比如退出登录,退出登录完成后,也使用ipc通知窗体部分,窗体接收到’logout-succ’后,执行关闭窗体的行为。// src/MyComponent.jsipc.on(‘mainWnd-close’, () => { // do something ipc.send(’logout-succ’);})·在开发项目时,可以先用网页的形式开发项目,等到网页项目部分差不多完成后,再注入electron中,开发网页项目部分和窗体部分的交互·在webpack中使用target: “electron-main"后,webpack将不会打包有关eletron的代码

January 31, 2019 · 1 min · jiezi

使用webpack打包多页jquery项目

虽然已经2019年了不过有一些项目还是需要用到jquery的不过考虑到使用jquery的一堆兼容性问题也为了可以顺利地使用ES6来撸代码研究使用webpack+babel打包代码来发布几个重点:1.为了将模块分割加载,不至于一个js文件过大,一个页面中使用多个js文件2.由于是多页项目(多个html),每个页面使用的js文件都不一致基于以上两点,需要配置多个入口文件3.会把小图片转换成base64,所以可能css转成的js文件会比较大,所以css文件都单独设置入口js例如,我们有三个页面:index、share、assist三个页面有通用的css文件:common.css设置入口文件时,可以这样设置entry: { // 通用css commoncss: path.resolve(__dirname, ‘./src/css/common.css.js’), // 主页 indexcss: path.resolve(__dirname, ‘./src/css/index.css.js’), index: path.resolve(__dirname, ‘./src/index.js’), // 页1 sharecss: path.resolve(__dirname, ‘./src/css/share.css.js’), share: path.resolve(__dirname, ‘./src/share.js’), // 页2 assistcss: path.resolve(__dirname, ‘./src/css/assist.css.js’), assist: path.resolve(__dirname, ‘./src/assist.js’),}其中,common.css.js文件中,只有几行代码import ‘../css/base.css’;import ‘../css/plugin.css’;import ‘../css/common.css’;common.css.js文件结束由于会有一些图片的base64,所以大小大约100+KB类似的还有index.css.js和share.css.js和assist.css.jsindex.css.js如下import ‘../css/index.css’;对,就一句话打包出来的js文件大小就看引入了多少小图片了,一般几百KB然后,要使用三个webpack的插件const HtmlWebpackPlugin = require(‘html-webpack-plugin’);const CopyWebpackPlugin = require(‘copy-webpack-plugin’);const jquery = require(‘jquery’);HtmlWebpackPlugin 用于打包出多个html文件CopyWebpackPlugin 用于img标签,后面说jquery 就是jquery,全局引用webpack.config.js里的plugins配置如下plugins: [ new webpack.ProvidePlugin({ $:“jquery” }), new CopyWebpackPlugin([{ from: __dirname + ‘/src/public/’ }]), // 吧src下public文件夹下的所有内容直接拷贝到dist(输出目录)下 new HtmlWebpackPlugin({ filename: ‘index.htm’, template: ‘src/index.html’, chunks: [‘commoncss’, ‘indexcss’, ‘index’], inject: ’true’, hash: true, }), new HtmlWebpackPlugin({ filename: ‘share.htm’, template: ‘src/share.html’, chunks: [‘commoncss’, ‘sharecss’, ‘share’], inject: ’true’, hash: true, }), new HtmlWebpackPlugin({ filename: ‘assist.htm’, template: ‘src/assist.html’, chunks: [‘commoncss’, ‘assistcss’, ‘assist’], inject: ’true’, hash: true, })]src目录下的文件如下index.js assist.js share.js是三个文件分别的入口文件index.html assist.html share.html是三个文件的模板,html代码可以写在这里(当然想用模板文件也是可以的,只要HtmlWebpackPlugin插件支持)dist文件夹如下(为什么是htm而不是html,是为了便于读者区分模板文件和输出文件)我们知道,webpack打包不会打包HtmlWebpackPlugin的template里的img标签下的图片,所以在html里使用了img标签的图片都要放在public文件夹下,CopyWebpackPlugin这个组件会直接把图片复制过去关于HtmlWebpackPlugin的具体参数的细则可以上网搜一下,很多这方面的内容其他的比如loader、babel不在这篇文章想说的重点之列,不赘述最后,附上webpack.config.js文件 let actName = ‘yourProjectName’;// let actKV = { name: actName, entry: { // 通用css commoncss: path.resolve(__dirname, ‘./src/css/common.css.js’), // 主页 indexcss: path.resolve(__dirname, ‘./src/css/index.css.js’), index: path.resolve(__dirname, ‘./src/index.js’), // 分享页1 sharecss: path.resolve(__dirname, ‘./src/css/share.css.js’), share: path.resolve(__dirname, ‘./src/share.js’), // 分享页2 assistcss: path.resolve(__dirname, ‘./src/css/assist.css.js’), assist: path.resolve(__dirname, ‘./src/assist.js’), } }; return { entry: actKV.entry, target: “web”, output: { path: path.resolve(__dirname + ‘/dist/’+actName), // publicPath: ‘.\’, filename: ‘js/[name].js’, // chunkFilename: “[name].chunk.[hash].js”, }, plugins: [ new webpack.ProvidePlugin({ $:“jquery” }), new CopyWebpackPlugin([{ from: __dirname + ‘/src/public/’ }]), new HtmlWebpackPlugin({ filename: ‘index.htm’, template: ‘src/index.html’, chunks: [‘commoncss’, ‘indexcss’, ‘index’], inject: ’true’, hash: true, }), new HtmlWebpackPlugin({ filename: ‘share.htm’, template: ‘src/share.html’, chunks: [‘commoncss’, ‘sharecss’, ‘share’], inject: ’true’, hash: true, }), new HtmlWebpackPlugin({ filename: ‘assist.htm’, template: ‘src/assist.html’, chunks: [‘commoncss’, ‘assistcss’, ‘assist’], inject: ’true’, hash: true, }) ], mode: ‘development’, node: { __filename: true, __dirname: true }, devtool: isProduction ? ‘source-map’:‘inline-source-map’, devServer:{ inline: true, open: true, historyApiFallback: true, // host: ip.address(), host: ’localhost’, progress: true, contentBase: “./dist/”, port: 3430, historyApiFallback:true, publicPath:’/src/’, proxy: { ‘’: { target: ‘http://127.0.0.1:3430’, secure: false } }, }, resolve: { alias: { }, extensions: [’.js’, ‘.less’, ‘.css’, ‘.vue’, ‘.jsx’], }, externals: { }, module: { rules: [{ test: /.vue$/, loader: ‘vue-loader’, }, { test: /.js$/, include: path.join(__dirname,’/src’), exclude: path.resolve(__dirname, ’node_modules’), use:[ { loader: ‘babel-loader’, query: { presets: [’es2015’] } } ] }, { test: /.xml$/, loader: “xml-loader” }, { test: /.(css|less)$/, loader: “style-loader!css-loader”, }, { test: /.(png|jpg|jpeg|gif|icon|webp)$/, loader: ‘url-loader’, options: { limit: 16384, name: ‘images/[name].[hash:5].[ext]’, } }, { test: /.(woff|woff2|svg|eot|ttf)??.$/, loader: “file-loader?&name=assets/fonts/[name].[ext]” }, { test: /.txt$/, loader: “text-loader” },{ test: /.jsx$/, exclude: /node_modules/, loaders: [‘jsx-loader’, ‘babel-loader’] }] }, } ...

January 31, 2019 · 2 min · jiezi

cross-env使用笔记

原文链接配置webpack遇到了要区分环境的问题,找到了 cross-env:npm安装方式npm i –save-dev cross-env在package.json{ “scripts”: { “dev”: “cross-env ASSETS_ENV=dev node build/build.js”, “build”: “cross-env NODE_ENV=prod node build/build.js” } }运行npm run build,这样NODE_ENV便设置成功,无需担心跨平台问题

January 31, 2019 · 1 min · jiezi

webpack入门总结

官网:https://www.webpackjs.com/核心原理一切皆模块按需加载概念module具有一定功能的模块。bundle打包后的文件。chunk打包过程分割的代码块。EntryWebpack 会递归的探索出 入口文件中所依赖的模块,并按照顺序 利用 Loader 进行处理。方式单入口1.stringentry: “app.js”;多入口1.Array<string>数组中的每一项都会被打包,形成互不依赖的文件entry: [“app.js”,“main.js”];2.objectentry: { [entryChunkName]: string | Array<string>}对象中的每一个属性都会被打包,形成互不依赖的文件entry: { index:[‘index.js’,‘app.js’], vendor: ‘vendor.js’}Output即是你配置了多个入口文件,你也只能有一个输出点。path输出文件的目录。绝对路径filename输出的文件名,它一般跟你entry配置相呼应。单入口自定义filename: “[name].bundle.js"多入口[name].js[id].js, 使用内部 chunk id[hash].js, 使用每次构建过程中,唯一的 hash 生成在项目中任何一个文件改动后就会被重新创建,然后webpack计算新的hash值[chunkhash].js, 使用基于每个 chunk 内容的 hashpublicPath公共资源路径publicPath 应该以/结尾,同时其它 loader 或插件的配置不能以/开头chunkFilename块,配置了它,非入口entry的模块,会帮自动拆分文件,也就是大家常说的按需加载,与路由中的 require.ensure相呼应。resolve配置模块如何解析。extensions:自动解析确定的扩展,省去你引入组件时写后缀的麻烦,alias:非常重要的一个配置,它可以配置一些短路径,module.rules - 编译规则rules:也就是之前的loaders,test : 正则表达式,匹配编译的文件,exclude:排除特定条件,如通常会写node_modules,即把某些目录/文件过滤掉,include:它正好与exclude相反,

January 29, 2019 · 1 min · jiezi

基于 React & TS & Webpack 的微前端应用模板

m-fe/react-ts-webpack在 Web 开发导论/微前端与大前端一文中,笔者简述了微服务与微前端的设计理念以及微前端的潜在可行方案。微服务与微前端,都是希望将某个单一的单体应用,转化为多个可以独立运行、独立开发、独立部署、独立维护的服务或者应用的聚合,从而满足业务快速变化及分布式多团队并行开发的需求。如康威定律(Conway’s Law)所言,设计系统的组织,其产生的设计和架构等价于组织间的沟通结构;微服务与微前端不仅仅是技术架构的变化,还包含了组织方式、沟通方式的变化。微服务与微前端原理和软件工程,面向对象设计中的原理同样相通,都是遵循单一职责(Single Responsibility)、关注分离(Separation of Concerns)、模块化(Modularity)与分而治之(Divide & Conquer)等基本的原则。fe-boilerplates 是笔者的前端项目模板集锦,包含了单模块单页面、单模块多页面、(伪)多模块单页面、微前端项目等不同类型的模板,其中微前端项目模块 m-fe/react-ts-webpack 与前者的区别即在于微前端中的各个模块能够独立开发,独立版本发布,独立部署,独立加载。分布式协作势必会带来协同以及开发流程上的挑战,在设计微前端项目架构的时候开发易用性也是非常重要的考量点。在年度总结中我也讨论了使用 TS 面向重构编程的意义,欢迎参考 Backend-Boilerplates/node 中的 ts-* 项目,使用 TS 进行全栈开发。当我们考量项目框架、模板或者脚手架的时候,首先想到的点就是希望尽可能对上层屏蔽细节,但是对于长期维护的、多人协作的中大型项目而言,如果项目的主导者直接使用了部分抽象的脚手架,不免会给未来的更新、迭代带来一定的技术负债;同时,目前也有很多成熟的工程化脚手架,因此笔者选择以项目模板的形式抽象出微前端中所需要的部分。尽可能地遵循简约、直观的原则,减少抽象/Magic Function 等;大型项目可能会抽象出专用的开发工具流,但是对于大部分项目而言,在现有框架/工具链的基础上进行适当封装会是较优选择。# 拉取并且提取出子项目git clone https://github.com/wxyyxc1992/fe-boilerplatecp fe-boilerplate/micro-frontend/react-ts-webpack ../# 添加全局的依赖更新工具$ yarn global add npm-check-updates# 为各个子项目安装依赖,以及链接各个子项目$ npm run bootstrap && npm run build# 执行预编译操作$ npm run build# 以基础模式运行 Host APP,此时 Host APP 作为独立应用启动$ cd packages/rtw-host-app & npm run dev:sa# 以标准模式运行子应用$ cd packages/rtw-mobx-app & npm run dev# 返回根目录$ cd .. & npm start值得说明的是,微前端作为概念对于不同人承载了不同的考量,其实现方式、落地路径也是见仁见智,若有不妥,敬请指教。Features非 APP 类可单独发布,APP 类可单独运行,与发布。发布版本可包含 ES, CJS, UMD 等,dist 目录下包含 ES/CJS 模块,build 目录下包含 APP 完整资源以及 UMD 模块。版本控制: 子应用资源不使用 Hash 方式,而是使用语义化版本,/[cdnHost]/[projectName]/[subAppName]/[x.y.z]/index.{js,css}样式,LESS 文件支持 CSS Modules,CSS/SCSS 使用标准 CSS状态管理,灵活支持 Redux/MobX/Dva 等不同的状态管理框架,对于 Redux 提供全局统一的 Store 声明Structure | 项目结构完整的微前端应用,可能会包含以下组成部分:Module | 模块: 模块是可单独编译、发布的基础单元,基础模式下可直接打包入主应用,标准模式下多项目共用时可单独打包为 AMD/UMD 格式,通过 SystemJS 引入Page | 页面: 页面不可单独编译,使用 Webpack SplitChunk 或其他机制进行异步加载App | 应用: 应用是对模块的扩展,是实际用户可见的部分Widget | 控件: 控件是特殊的模块,譬如通用的无业务组件等Extension | 扩展: 扩展是特殊的应用,提供了跨模块的通用功能,类似于 Chrome Extension 的定位基于此,我们可以将某个微前端应用抽象为如下不同的模块组:基础模块:rtw: 根目录,public 目录下包含了部分跨模块集成测试的代码核心模块:rtw-core/rtw-sdk/rtw-shared: 暴露给子应用可用的通用基础类、模型定义、部分无界面独立模块等。rtw-core 建议不放置界面相关,使用 Jest UT 方式进行功能验证。rtw-bootstrap: 完整项目级别编译与启动入口,包含项目的运行时配置、依赖配置消息总线、注册中心、核心模块加载机制等。rtw-host-app: 提供界面基础容器,譬如应用标准的 Layout,Menu 等组件;提供 Redux 核心 Store。子业务应用:rtw-mobx-app: MobX 示例应用rtw-redux-app: Redux 示例应用扩展模块:rtw-widgets: 包含部分业务型控件,提供给所有的子应用使用,提取通用业务逻辑、对上屏蔽部分第三方依赖关系,类似于完整的 OSS 文件上传控件等。rtw-extensions: 包含部分业务无关的通用型插件,类似于 Chrome Extension 的定位。rtw-worker: 包含通用的 Web Worker & WASM 计算模块,子应用内也可以通过 Buffer 方式直接引入自定义的 Worker如果希望在子应用 A 中加载子应用 B 的实例,则应该使用类似于依赖注入的方式,从统一的注册中心中获取该实例对象。所有各个模块共享的基础库,都必须以 UMD 模式加载到全局;rtw-host-app 中声明与使用需要展示哪些模块,rtw-bootstrap 中注册可提供的 UMD 子模块。开发模式笔者一直推崇渐进式的工程架构,因此该模板对于复杂度要求较低的项目而言,可以直接从基础模式启动,与其他 TS 项目并无太大区别。基础模式基础模式类似于(伪)多模块单页面,仅有唯一的 Host APP 作为编译与运行的入口,其他包体(譬如 rtw-core)直接打包进主包体中,不使用 SystemJS 进行独立加载。rtw-corertw-core 及相似的库承载了公共的结构定义、工具类等,在该包体目录下运行 npm run build 命令即可以生成 ES/CJS/UMD 等多种类型文件,以及 types 类型定义;可以直接通过 npm publish 来发布到公共/私有的 NPM 仓库中。其他包体通过 NPM 安装 rtw-core 并使用,如果以标准模式运行,则需要首先加载该库到全局作用域,利用 RequireJS/SystemJS 等工具遵循 AMD 规范来注入到其他依赖的库/应用中。值得一提的是,对于子应用中,如果存在需要共享组件/类型的情景。对于类型信息,建议是将子应用同样编译打包发布到 NPM 仓库中,纯组件可以直接引入,对于业务组件建议通过全局的注册中心来获取。rtw-host-app在 rtw-host-app 包下,执行 npm run dev:sa 命令,会从 src/index.sa 文件启动应用;如上文所述,该模式仅会基于 Webpack Splitted Chunk 进行异步加载,其开发流程与标准的单模块应用并无区别。标准模式rtw-bootstrap & rtw-host-apprtw-bootstrap 是微前端应用的实际启动点,其核心功能是执行依赖与子应用的注册。在启动时,其会根据传入的 HOST_APP 与 DEV_APP 等变量信息完成应用的顺序加载与启动。在标准模式下,rtw-host-app 的入口是 src/index 文件,该模式下,index 文件会对外暴露 render 函数,该函数会由 rtw-bootstrap 注入 importApp 函数来执行子应用加载:export function render(_importApp: Function) { importApp = _importApp; ReactDOM.render( … );}换言之,rtw-bootstrap 提供了应用加载的能力,而 rtw-host-app 决定了应该加载哪些应用;在实际案例中,我们应该将用户权限控制、菜单与子应用信息获取等业务操作放置在 rtw-host-app 中。rtw-redux-app & rtw-mobx-app这里以 rtw-mobx-app 为例介绍如何进行子应用开发,如果是项目已经发布上线,那么我们可以通过 Resource Overrides 等在线资源请求转发的工具将线上资源请求转发到本地服务器。在进行本地开发时,因为子应用本身并不会包含 ReactDOM.render 或者类似的将 Virtual DOM 渲染到界面的函数,因此在运行 npm run dev 之后,本地会开启生成 UMD 文件的 Webpack Dev Server。参考子应用的 public/index.html 文件:<script src="./bootstrap/static.js" type=“text/javascript”></script><script src="./bootstrap/runtime.js" type=“text/javascript”></script><script src="./bootstrap/vendors.js" type=“text/javascript”></script><script> // 联调环境 // window.HOST_APP = { // id: ‘host’, // name: ‘HOST APP’, // module: ‘http://0.0.0.0:8081/index.js’, // css: ‘http://0.0.0.0:8081/index.css’ // }; // 正式开发环境 window.HOST_APP = { title: ‘HOST APP’, module: ‘/release/rtw-host-app/index.js’, css: ‘/release/rtw-host-app/index.css’ }; window.DEV_APP = { id: ‘dev’, name: ‘DEV APP’, module: ‘/index.js’ };</script><script src="./bootstrap/index.js" type=“text/javascript”></script>可以看出子应用的启动需要依赖于 rtw-bootstrap 以及 rtw-host-app,如果项目已经发布上线,那么建议是直接从 CDN 加载资源;否则可以将资源放置到 public/release 目录下。如果本地需要同时调试 Host APP,则直接也将 Host APP 以开发方式运行(npm run dev),然后直接引入 Webpack Dev Server 生成的资源地址即可。延伸阅读如果希望实践掌握 Web 开发,可以阅读 JavaScript CheatSheet/ProgrammingLanguage-Series/JavaScript,DOM CheatSheet/CSS CheatSheet,React CheatSheet/Vue CheatSheet,现代 Web 开发基础与工程实践/Web Tuning CheatSheet 等。 ...

January 29, 2019 · 2 min · jiezi

webpack4小白学习全过程记录

webpack :现代 JavaScript 应用程序的静态模块打包器前言:本文描述一个小白学习webpack4从零开始的完整过程全纪录,可以说完全以一个初学者的口吻来写这篇文章的,与此同时这篇文章还会长期更新,不足之处还望大佬雅正!大白话讲webpack对于小白来说,看到一些专业的术语就脑壳疼什么是webpack?简单来讲,就是把我们写好的代码打包。怎么个打包法,压缩代码,相同资源抽取,处理一下浏览器不识别的代码(如ES6+、TS、SASS等)。一句话,用不到就扔掉,能压缩就压缩,能兼容就兼容初探webpack①、安装npmnpm是世界上最大的软件注册表,使用npm我们可以快速安装各种大小型软件和库下载:在node.js中都自带了npm,所以我们下载node.js即可node.js官网: https://nodejs.org/en/下载完后,我们可以在dos命令行下可以看到node.js的版本和npm的版本 node -v npm -v②、建好目录架构①、新建练习文件目录 ex1①、在ex1目录下新建源文件目录 src②、在src目录下新建入口文件 index.js 形成的目录结构如下:③、初始化package.jsonpackage.json文件定义了这个项目所需要的各种模块,以及项目的配置信息(比如名称、版本、许可证等元数据)。在ex1目录下使用命令:npm init -yinit指令会询问一系列的问题,并将你的配置写成一个package.json文件。如果使用了-f|–force|-y|–yes这些参数,那么会生成一个默认的package.json文件。这个init过程很快,可以看到在ex1目录下多了个package.json文件④、安装webpack提示:webpack4.0以上版本的webpack的脚手架webpack-cli已经被分离出来,需要另外安装。安装webpack分为全局安装和本地安装(局部安装)全局安装(不推荐) npm install webpack webpack-cli -g (-g就是全局安装) 本地安装 npm install webpack webpack-cli -D (-D 就是安装到开发环境)安装成功后如图所示:再来看下我们的文件目录发生了什么变化:node_modules文件夹主要是用于放用包管理工具下载安装了的包,这个文件的内容有很多package-lock.json锁定了包的版本,确保能够避免包版本不同产生的问题。⑤、使用npx进行打包npx 会帮你执行依赖包里的二进制文件。npx会自动找到node_modules/bin 的webpack.cmd去执行,免去了script的配置在src的目录下新建一个名为 demo1.js的文件在demo1.js 中写入如下内容module.exports = ‘这是common.js的语法,在node端运行,把我用webpack打包之后,就可以在浏览器上运行了’在index.js中写入如下内容let d1 = require(’./demo1.js’);console.log(d1); 使用命令进行打包npx webpack结果如图:打包后我们可以发现在ex1目录下多了个 dist 文件夹这个dist 文件夹中有个 main.js文件,是打包后的js文件为了查看结果,我们可以在dist目录下新建一个 index.html 文件在index.html使用script标签引入main.js <script src="./main.js"></script>在控制台里面就可以看到这么一句话:‘这是common.js的语法,在node端运行,把我用webpack打包之后,就可以在浏览器上运行了’细心的你可能会发现上面打包的结果有一个 黄色的警告WARNING in configurationThe ‘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.You can also set it to ’none’ to disable any default behavior. Learn more: https://webpack.js.org/concep…黄色警告意为webpack没有指定是开发模式development 还是 生产模式production那我们就可以在命令后面加上 –mode development看,是不是没有了黄色警告提示。npx的缺点:虽然使用npx可以快速的帮我们打包,但是不够灵活,比如打包后文件夹名字都是固定 dist,因此我们需要定义webpack打包的配置文件,这样才可以随心所欲的打包webpack配置①、新建配置文件我们现在ex1的目录下新建一个名为 webpack.config.js 文件由于webpack是基于node,所以要遵循common.js规范②、入口(entry)入口起点(entry point)指示 webpack 应该使用哪个模块,来作为构建其内部依赖图的开始。进入入口起点后,webpack 会找出有哪些模块和库是入口起点(直接和间接)依赖的。entry 属性的单个入口语法module.exports = { entry: ‘./src/index.js’};③、出口(output)output 属性告诉 webpack 在哪里输出它所创建的 bundles,以及如何命名这些文件。默认值为 ./dist。在 webpack 中配置 output 属性的最低要求是,将它的值设置为一个对象,包括以下两点:filename 用于输出文件的文件名。目标输出目录 path 的绝对路径。④、入口和出口的整合webpack.config.js// 基于node,遵循common.js规范const path = require(‘path’) // 引入node.js Path 模块module.exports = { entry: ‘./src/index.js’, // 入口 output: { filename: ‘bundle.js’, path: path.join(__dirname, ‘./build’) }}由于path要求为绝对路径,所以我们使用了 path.join 拼接路径这个node的语法,node不能在浏览器端运行,所以我们可以利用 vscode 编辑器 的 Code Runner 插件来查看当前路径的结果在webpack.config.js保留如下代码:const path = require(‘path’) // 引入node.js Path 模块console.log(path.join(__dirname, ‘./build’));结果如图:在webpack.config.js配置好出入口后,那我们可以尝试着打包了打包结果如图:在ex1的目录下多了个 build文件以及其目录下的bundle.js文件那么说明了我们的配置是成功的!⑤、npm脚本可以参考阮一峰老师的博客:http://www.ruanyifeng.com/blo…npm 允许在package.json文件里面,使用scripts字段定义脚本命令。npm 脚本。它的优点很多项目的相关脚本,可以集中在一个地方。不同项目的脚本命令,只要功能相同,就可以有同样的对外接口。用户不需要知道怎么测试你的项目,只要运行npm run test即可。 可以利用npm 提供的很多辅助功能。在我们原有的package.json中,我们可以发现有一个这样"scripts"字段"scripts": { “test”: “echo "Error: no test specified" && exit 1”}因此我们可以尝试使用 npm test 命令看下会有什么结果结果执行了echo语句我们可以利用npm脚本来替代webpack语句,我们修改一下test字段对应为webpack命令"scripts": { “test”: “webpack”}接着我们尝试使用 npm test 命令来运行一下可以发现我们已经打包成功了。那么有的朋友就会问了,一定要test吗?好,我们随便来个命令,就 abc在test字段下,我们加多一个abc字段,如下面代码所示:“scripts”: { “test”: “echo "Error: no test specified" && exit 1”, “abc”: “webpack”}执行 npm abc 命令看看结果怎样哎,奇怪了呀,这是什么情况,有一点很明确的是,并没有执行webpack打包执行命令。不急不急,我们先来看下提示先:注意到这句话:where <command> is one of:也就是说我们的命令应该下面那些,也就是说只能使用 npm <command>,这个command应该是提示其中之一,我们在提示中可以发现有 test 命令也就是说只要满足了上面字段的其中之一,就可以使用 npm <command>但是这些命令是npm自带的,并不是所有都可以用来执行script脚本的。除此之外,还有一个start命令也是可以直接执行的,如图"scripts": { “test”: “echo "Error: no test specified" && exit 1”, “start”: “webpack”}那么问题来了,我们怎么知道哪些命令可以直接执行script,要一个个尝试岂不是很麻烦吗?此时我们可以使用npm run <command> 命令来直接执行脚本使用npm run script执行脚本的时候都会创建一个shell,然后在shell中执行指定的脚本。所以这次我们不用再为使用什么script字段而烦恼了。好,我们再来试一下abc字段"scripts": { “test”: “echo "Error: no test specified" && exit 1”, “abc”: “webpack”}哈哈,终于成功了!⑥、模式设置在之前我有讲到过,黄色警告是由于没有设置模式而出现,那时候提的方法是每次都要在执行打包的命令的时候加上这么一句 –mode development/production那这样是不是很麻烦,所以我们可以在配置文件这样定义mode: ‘development’那么,此时我们的webpack.config.js的文件内容如下:// 基于node,遵循common.js规范const path = require(‘path’) // 引入node.js Path 模块module.exports = { entry: ‘./src/index.js’, // 入口 output: { filename: ‘bundle.js’, path: path.join(__dirname, ‘./build’) }, mode: ‘development’ // 模式}此时,我们再打包试一下,结果如图:我们可清楚地看到黄色警告已经去掉了webpack入门①、关系依赖在我们的package.json文件里,可以看到这么一个字段 “devDependencies"除此之外还有一个与之相关的字段叫 “dependencies ”*devDependencies 与 dependencies 的区别devDependencies 里面的模块只用于开发环境(development) 而 dependencies 里面的模块是需要发布到生产环境(production) 。比如我们有一个项目要用到jQuery,在线上运行的时候如果没有jQuery包的依赖运行就会报错,这时候就应该把这个依赖写入dependencies 而我们使用的一些构建工具如webpack这些只是在开发中使用的包,上线以后就和这些包没关系了,所以将这种依赖写入 devDependencies在我们使npm安装模块的时候,都会使用一个命令npm install(可以简写为 npm i)与此对应的还有这两个重要的对应参数:–save (可以简写为-S)–save-dev (可以简写为-D)①、npm install m将m模块安装到node_modules目录中不会修改package.json运行npm install命令时,不会安装m模块②、npm install m –save:把m模块安装到node_modules目录中会在package.json的 dependencies 属性下添加m模块运行npm install命令时,自动安装m模块到node_modules目录中②、npm install m –save-dev:把m模块安装到node_modules目录中会在package.json的 devDependencies 属性下添加m模块运行npm install命令时,会自动安装到node_modules目录中②、本地服务器(devServer)webpack-dev-server 能够用于快速开发应用程序里面包含了很多丰富的功能,其中最为重要的当然是能够监听本地代码的修改,利用热加载自动刷新浏览器,这样大大地方便了我们进行代码的调试第一步:安装webpack-dev-server在前面,我有讲到关系依赖这方面的知识,关于 devDependencies 与 dependencies 的区别,相信大家都能看懂。很明显 webpack-dev-server 只是用来本地调试的,上线之后就用不上了,那么我们可以很明确将webpack-dev-server 放到devDependencies中执行如下命令:npm install webpack-dev-server -D安装好后,我们再来看一下package.json文件的变化第二步:配置webpack-dev-server配置项目描述注意contentBase告诉服务器从哪个目录中提供内容推荐使用绝对路径。compress一切服务都启用 gzip 压缩compress: truehost指定使用一个 host。默认是 localhost可以外部服务器访问https默认情况下,dev-server 通过 HTTP 提供服务https: trueopen启用 open 后,dev server 会打开浏览器默认浏览器,可以指定其他port指定要监听请求的端口号默认8080其实webpack-dve-server的配置项目远远不止这些,这里只是列举了一部分我们先在package.json中添加script脚本"dev”: “webpack-dev-server"①、contentBase解析再回来看下我们的webpack.json.js文件,webpack-dve-server配置的字段为devServer,我们先不配置先,看看会怎么样?此时webpack.json.js的内容// 基于node,遵循common.js规范const path = require(‘path’) // 引入node.js Path 模块module.exports = { entry: ‘./src/index.js’, // 入口 output: { filename: ‘bundle.js’, path: path.join(__dirname, ‘./build’) }, mode: ‘development’ // 模式}执行命令:npm run dev看下结果:现在可以清楚的看到,我们的项目已经运行在 http://localhost:8080好的,那么我们在浏览器输入这个url看看这个页面会显示什么如图所示:这里只显示了我们整个文件的目录默认情况下,contentBase将使用当前工作目录作为提供内容的目录为了证明这点,我们在ex1的目录下新建一个index.html现在的目录结构如下:现在在根目录下的index.html写入如下内容:<!DOCTYPE html><html lang=“en”><head> <meta charset=“UTF-8”> <title>index</title></head><body> 这是根目录的index.html</body></html>好,现在在重新执行命令 npm run dev然后在浏览器打开http://localhost:8080 页面我们就可以发现结果不同了那么这就可以证明了默认情况下,contentBase将使用当前工作目录作为提供内容的目录好,现在我们要把内容目录设在build文件夹下我们现在build目录下新建一个index.html文件写入如下内容:<!DOCTYPE html><html lang=“en”><head> <meta charset=“UTF-8”> <title>Document</title></head><body> 这是build文件下的index.html</body></html>这时候回到我们的配置文件(webpack.config.js)写入如下内容:devServer: { contentBase: ‘./build’}现在webpack.config.js的内容如下:// 基于node,遵循common.js规范const path = require(‘path’) // 引入node.js Path 模块module.exports = { entry: ‘./src/index.js’, // 入口 output: { filename: ‘bundle.js’, path: path.join(__dirname, ‘./build’) }, mode: ‘development’, // 模式 devServer: { contentBase: ‘./build’ }}好,现在万事俱备,只欠东风。现在我们再重新执行 npm run dev 命令同样的,在浏览器可以看到结果②、open启用 open 后,devServer 会打开浏览器。在devServer设置如下:open: true注意,这里的true是布尔值,不是字符串在启用命令 npm run dev后,devServer 会打开默认浏览器大家用window一般都是IE浏览器作为默认浏览器那么此时如果我们想使用谷歌浏览器来启动应该怎么设置呢?此时,我们可以在package.json对script脚本进行设置,// 打开默认浏览器"dev”: “webpack-dev-server –open”// 打开谷歌浏览器"dev": “webpack-dev-server –open chrome”// 打开火狐浏览器"dev": “webpack-dev-server –open firefox”*提示:如果在script设置了–open,那么可以不用在配置文件设置open字段了③、source map由于使用webpack打包后,原来的代码经过压缩,去空格,bable的编译之后,已经变成我们不认识的代码了,为了方便我们调试调式代码,定位bug,source map就出现了在webpack.config.js中,devtool选项用于控制是否生成,以及如何生成 source map。loader入门css loader安装loader 用于对模块的源代码进行转换。loader 可以使你在 import 或"加载"模块时预处理文件。webpack本身只能处理javascript文件,而对于css文件需要loader来处理对于css处理,我们常用的有这两种loader:css-loader 和 style-loader使用命令安装这两个loadernpm i -D style-loader css-loader接下来在src文件下新建名为 style 的文件夹,在style 下新建名为 index.css 文件为了让效果更加直观,往 index.css 文件写入如下内容body{background-color: yellowgreen;}接着在我们 src目录下的 index.js 文件中使用ES6的模块语法 import 将index.css文件导入进来import ‘./style/index.css’我们先不配置loader,看看浏览器会提示什么执行 npm run dev 命令,看看会发生什么,如图所示:提示模块解析失败,需要loader进行处理loader配置有三种使用 loader 的方式:配置(推荐):在 webpack.config.js 文件中指定 loader。内联:在每个 import 语句中显式指定 loader。CLI:在 shell 命令中指定它们。我们选择在webpack.config.js中配置loader基本写法如下:module: { rules: [ { test: /.css$/, // 利用正则表达式匹配文件名后缀为css的文件 use: [‘style-loader’, ‘css-loader’] // 一组loader的执行顺序默认是从右向左 } ]}接着,我们再重新执行 npm run dev命令,页面如图所示:说明我们css文件有效果了,再来看下网页元素效果如图,可以看到是以内联样式插入到html插件(plugins)入门插件目的在于解决 loader 无法实现的其他事。html-webpack-plugin安装这个插件用来简化创建服务于 webpack bundle 的 HTML 文件,由于通常在打包中包含一些含有hash值的名字,而这些名字在每次编译的时候都发生变化的时候对我们开发人员很麻烦,利用这个插件,我们可以利用这个插件捆绑式这些文件方便我们的开发。执行安装命令npm i -D html-webpack-plugin使用html-webpack-plugin在webpack.config.js 配置如下:// 基于node,遵循common.js规范const path = require(‘path’) // 引入node.js Path 模块const HtmlWebpackPlugin = require(‘html-webpack-plugin’) // 引入HtmlWebpackPluginmodule.exports = { entry: ‘./src/index.js’, // 入口 output: { filename: ‘bundle.js’, path: path.join(__dirname, ‘./build’) }, mode: ‘development’, // 模式 devServer: { contentBase: ‘./build’ }, module: { rules: [ { test: /.css$/, // 利用正则表达式匹配文件名后缀为css的文件 use: [‘style-loader’, ‘css-loader’] // 一组loader的执行顺序默认是从右向左 } ] }, plugins: [ new HtmlWebpackPlugin() ]}在这里,我们这个插件做了两步工作:const HtmlWebpackPlugin = require(‘html-webpack-plugin’)plugins: [ new HtmlWebpackPlugin()]接下来,我们需要在src目录下新建模板文件,名为 template.html,我们打包后的文件都已这个html文件为模板在模板html写入如下内容:<!DOCTYPE html><html lang=“en”><head> <meta charset=“UTF-8”> <title>模板文件</title></head><body> 使用模板文件</body></html>这里我们只是简单的使用两个参数选项描述注意filename就是html文件的文件名,默认是index.html template生成的文件所依赖文件模板的路径 plugins的配置如下plugins: [ new HtmlWebpackPlugin({ filename: ‘index.html’, template: ‘./src/template.html’, })]好的,现在所有都配置好了,我们来测试一下执行命令 npm run dev可以发现,运行项目时使用的是模板文件现在,再来测试一下打包文件我们把原来的build文件删除(当然可以不删,因为会覆盖)执行命令 npm run abc此时,我们可以发现 build 目录下 index.html是这个样子的直接就帮我们用script标签插入了bundle.js总结:入门不易,掌握更难,纸上得来终觉浅,绝知此事要躬行! ...

January 28, 2019 · 3 min · jiezi

webpack4系列教程(十):总结

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

January 27, 2019 · 1 min · jiezi

webpack的简单实例学习

webpack工具的使用一、实现js打包1.1、创建项目testwebpack// 在文件夹 testwebpack// 初始化 package.jsonnpm init// 安装项目依赖 node_modulesnpm install webpack –save-dev1.2、创建文件夹app 和 build app:放源代码 build: 编译之后的输出路径 1.2.1、app文件夹内创建 app.js和hello.js 代码编写遵循 nodejs的 commonjs 规范// app.js// exports 导出创建的标签 hellomodule.exports = function(){ var hello = document.createElement(“div”); hello.textContent = ‘hello webpack’; return hello;}// build.js// require 引入var hello = require("./hello.js");// 将标签放到root中document.getElementById(“root”).appendChild(hello); 1.2.2、build文件夹内创建 index.html<body> <!– 根容器 –> <div id=“root”></div></body>1.3、使用 webpack进行编译,将app文件中源代码编译到build文件中// webpack 版本4 和 webpack 版本2 的写法不一样。主要注意// webpack2 app/app.js 是要编译的文件 build/bundle.js 是要输出文件 // webpack app/app.js build/bundle.js// webpack4 npx webpack app/app.js –output-filename build/bundle.js –output-path . –mode development 编译时可能出现报错:1、报错webpack不是内部命令–需要全局安装webpack & webpack-clinpm install webpack -gnpm install webpack-cli -g2、报错WARNING in configurationThe ‘mode’ option has not been set, webpack will fallback to ‘production’ for this value. Set ‘mode’ option to ‘development’ or ‘production’ to enable defaultsfor each environment.You can also set it to ’none’ to disable any default behavior. Learn more: https://webpack.js.org/concepts/mode/ERROR in multi ./app/app.js build/bundle.jsModule not found: Error: Can’t resolve ‘build/bundle.js’ in ‘E:\test\1webpack’ @ multi ./app/app.js build/bundle.js main[1]–查看安装webpack的版本,我安装的版本4,执行了webpack2的编译方法。需要执行webpack4的编译方法,不然会出错。1.4、编译完成后会在build文件中生成bundled.js文件引入到index.html 中就可以用了。<script type=“text/javascript” src=“bundle.js”></script> ...

January 27, 2019 · 1 min · jiezi

搭建webpack简易脚手架

关键词 vue typescript webpack本文是为下一篇《3分钟搞定 Vue + TypeScript开发》文章做的知识铺垫,文章结尾提供完整的github示例代码库。准备已经掌握vue开发的情况下,想体验一下TypeScript开发vue,可以通过以下过程配置一个脚手架。1.阅读webpack官网文档 https://www.webpackjs.com/webpack工作原理入口起点(entry points)输出(output)模式(mode)loader插件(plugins)配置(configuration)模块(modules)模块解析(module resolution)依赖图(dependency graph)manifest构建目标(targets)模块热替换(hot module replace)2.阅读TypeScript官网文档 https://www.tslang.cn/docs/ho…脚手架搭建了解辅导教程部分5分钟上手TypeScriptASP.NET CoreGulpJavaScript迁移React & Webpack3.阅读Vue官网文档vue配置入门: https://cn.vuejs.org/v2/guide…开始配置npm < 5.x 建议升级到更高版本(当前稳定版本6.7.0), 或使用yarn来管理包配置文件完成准备工作后下面就要进行这3类文件的配置package.jsonwebpack.config.jstsconfig.json配置思路,渐进式配置避免过程中问题堆积,先让脚手架工作起来首先要让webpack能运行起来安装Vue框架, 配置Vue、TS编译所需的loader和plugins让webpack运行起来这是一个使webpack能工作的最小配置。当写好一个webpack配置, 从最简单的一步开始感受一下webpack, 建立亲切感往后就容易多了。webpack配置文件:/** * @file ./config/webpack.config.js * @author CharlesYu01 */module.exports = { entry: ‘./index.ts’, mode: ‘production’};编译执行结果:$ webpack –config ./config/webpack.config.jsHash: 067221c5690968574418Version: webpack 4.29.0Time: 86msBuilt at: 2019-01-26 14:05:49 Asset Size Chunks Chunk Namesmain.js 930 bytes 0 [emitted] mainEntrypoint main = main.js[0] ./index.ts 0 bytes {0} [built]可以看一下编译出来的内容,默认在./dist/main.js中。ps:恩,你已经掌握了webpack最核心的玩法了,处理更复杂的工作时再去了解loader、plugins的原理。用Vue+TS编写一个可浏览的页面安装插件// package.json // TODO: 掌握各插件的作用…“devDependencies”: { “awesome-typescript-loader”: “^5.2.1”, “html-webpack-plugin”: “^3.2.0”, “source-map-loader”: “^0.2.4”, “ts-loader”: “^5.3.3”, “typescript”: “^3.2.4”, “vue-class-component”: “^6.3.2”, “vue-loader”: “^15.6.1”, “vue-tsx-support”: “^2.2.2”, “webpack”: “^4.29.0”, “webpack-cli”: “^3.2.1”, “webpack-dev-server”: “^3.1.14”},“dependencies”: { “vue”: “^2.5.22”, “vue-property-decorator”: “^7.3.0”}…webpack安装时如有异常,使用官网推荐方法 yarn add webpack –dev添加html入口文件有了编译产出的js,还需要将js引入到页面中,此时使用webpack plugins配置一个插件 html-webpack-plugin , 就可以完成html页面引入js的功能。添加vue template 的编译能力 vue-loader引用vue官网:运行时 + 编译器 vs. 只包含运行时如果你需要在客户端编译模板 (比如传入一个字符串给 template 选项,或挂载到一个元素上并以其 DOM 内部的 HTML 作为模板),就将需要加上编译器,即完整版:new Vue({ template: ‘<div>{{ hi }}</div>’})// 不需要编译器new Vue({ render (h) {return h(‘div’, this.hi)}})// 配置webpack.config.jsmodule.exports = { resolve: { alias: { ‘vue$’: ‘vue/dist/vue.esm.js’ }}}添加一个可预览的webServer webpack-dev-serverdevServer: { contentBase: ‘../dist’, port: 9000}#### 验证结果1.用TypeScript实现一个Input组件 /example/vue-tsx/Input.tsx import {VNode} from ‘vue/types’; import Component from ‘vue-class-component’; import * as Vue from ‘vue-tsx-support’; import { Prop } from ‘vue-property-decorator’; export interface InputProps { placeholder: String } @Component export default class Input extends Vue.Component<InputProps,any> { [x: string]: any; text: any; input(e) { this.text = e.target.value this.$emit(‘input’, e.target.value); } @Prop([String, Boolean]) value: string | boolean | undefined; data() { return { text: ’’ } } render() { return <input value={this.value} onInput={this.input}/> } }2.引用组件 new Vue({ template: ‘<div>组件<Input value="" @input=“input”/>{{message}}</div> ‘, data:{ message:‘hello Vue!’ }, methods:{ input:function(e) { this.message = e.target.value; } } }).$mount(’#app’);3.预览// 可以全量安装一次项目依赖yarn installyarn start$ webpack-dev-server –config ./config/webpack.config.jsℹ 「wds」: Project is running at http://localhost:9000/ℹ 「wds」: webpack output is served from /ℹ 「wds」: Content not from webpack is served from ../distℹ 「wdm」: Hash: 9bf165f80a4d3c7600c0Version: webpack 4.29.0Time: 2129msBuilt at: 2019-01-26 19:49:49 Asset Size Chunks Chunk Names./index.html 409 bytes [emitted] main.js 662 KiB main [emitted] main Entrypoint main = main.js [0] multi (webpack)-dev-server/client?http://localhost:9000 ./example/index.ts 40 bytes {main} [built] [./example/index.ts] 471 bytes {main} [built] [./node_modules/ansi-html/index.js] 4.16 KiB {main} [built] [./node_modules/ansi-regex/index.js] 135 bytes {main} [built] [./node_modules/events/events.js] 13.3 KiB {main} [built] [./node_modules/html-entities/index.js] 231 bytes {main} [built] [./node_modules/loglevel/lib/loglevel.js] 7.68 KiB {main} [built] [./node_modules/strip-ansi/index.js] 161 bytes {main} [built] [./node_modules/url/url.js] 22.8 KiB {main} [built] [./node_modules/vue/dist/vue.esm.js] 289 KiB {main} [built] [./node_modules/webpack-dev-server/client/index.js?http://localhost:9000] (webpack)-dev-server/client?http://localhost:9000 7.78 KiB {main} [built] [./node_modules/webpack-dev-server/client/overlay.js] (webpack)-dev-server/client/overlay.js 3.58 KiB {main} [built] [./node_modules/webpack-dev-server/client/socket.js] (webpack)-dev-server/client/socket.js 1.05 KiB {main} [built] [./node_modules/webpack/hot sync ^./log$] (webpack)/hot sync nonrecursive ^./log$ 170 bytes {main} [built] [./node_modules/webpack/hot/emitter.js] (webpack)/hot/emitter.js 75 bytes {main} [built] + 15 hidden modules Child html-webpack-plugin for “index.html”: 1 asset Entrypoint undefined = ./index.html [./node_modules/html-webpack-plugin/lib/loader.js!./example/index.html] 618 bytes {0} [built] [./node_modules/lodash/lodash.js] 527 KiB {0} [built] [./node_modules/webpack/buildin/global.js] (webpack)/buildin/global.js 472 bytes {0} [built] [./node_modules/webpack/buildin/module.js] (webpack)/buildin/module.js 497 bytes {0} [built] ℹ 「wdm」: Compiled successfully.效果示例代码:https://github.com/CharlesYu0…TODO:后续增加更多webpack构建示例基于vue cli3.0开发一套适合多种项目结构使用的插件化vue脚手架 (预计19年7月份) ...

January 26, 2019 · 3 min · jiezi

用webpack4从零开始构建react开发环境

项目文件准备:执行npm init,然后创建如下图所示的文件。在index.html里面添加<!DOCTYPE html><html> <head> <title>The Minimal React Webpack Babel Setup</title> </head> <body> <div id=“app”></div> <script src="./bundle.js"></script> </body></html>在webpack.config.js里面添加module.exports = { entry: ‘./src/index.js’, output: { path: __dirname + ‘/dist’, publicPath: ‘/’, filename: ‘bundle.js’ }, devServer: { contentBase: ‘./dist’ }};在package.json里面添加 “scripts”: { “start”: “webpack-dev-server –config ./webpack.config.js –mode development” },这样,当执行npm start的时候,就会使用webpack-dev-server把index.js相关文件打包,生成bundle.js,这时候浏览器会打开一个窗口,执行index.html(contentBase里面定义了),又因为index.html里面引入了bundle.js,就可以把压缩后的js文件执行起来。当然引入bundle.js这一步可以由我们强大的html-webpack-plugin完成。安装依赖npm install –save-dev webpack webpack-dev-server webpack-clinpm install –save-dev @babel/core @babel/preset-envnpm install –save-dev babel-loadernpm install –save-dev @babel/preset-react配置babel在根目录下新建.babelrc文件,然后添加{ “presets”: [ “@babel/preset-env”, “@babel/preset-react” ]}在webpack.config.js里面添加babel-loader配置module.exports = { … module: { rules: [ { test: /.(js|jsx)$/, exclude: /node_modules/, use: [‘babel-loader’] } ] }, resolve: { extensions: [’*’, ‘.js’, ‘.jsx’] } …};这个时候执行npm start,就可以在浏览器访问http://localhost:8080看到Index.html里面的内容啦啦。 ...

January 25, 2019 · 1 min · jiezi

教你写个webpack的loader

loader介绍loader是将指定格式的资源文件转化成一定格式输出,例如sass-loader将scss文件转化成css文件, babel-loader将ES6转化成ES5.一个loader的结构就是一个函数,最简单loader如下:module.exports = function(content){ // content就是你要处理文件的内容,如处理App.vue文件,content就是你在App.vue写的代码 //… 中间可以对content处理 return content // 这里你也可以用this.callback(null, content)导出}loader的使用,配置webpack文件 { test: /.css$/, // easy-css-loader就是下面我要写的loader use: [‘style-loader’, ‘css-loader’, ’easy-css-loader’] }loader解析顺序是:从右向左, 上一个loader处理完的content传递给下一个loader, 一般每种loader功能是单一的easy-css-loader实现easy-css-loader将实现一些css代码的简写(虽然sass等已经有这个功能,但是每次都要写个.scss文件在导入,不大方便).效果图例:对应代码<div> <div class=“easy-css”>2</div></div><style >.easy-css{ border: 1px solid red; posC; wh(100px, 80px); font(20px, blue, center); comB(http://ww1.sinaimg.cn/large/b44313e1gy1fyz1li77jzj20q411wdop.jpg); }</style>下面讲讲font(..)解析过程<!–你代码写的样式–><style > .easy-css{ font(20px, yellow); }</style><!– 会解析成 –><style > .easy-css{ font-size: 20px; color: yellow; text-align: left; }</style>上面的代码, 如果在webpack配置解析.css的自定义的loader, 那么在loader里,前面说的content就长成这样了 .easy-css{ font(20px, yellow); }是不是只要正则匹配font(20px, yellow),替换成font-size: 20px;color: yellow;text-align: left;下面是loader的代码/** * @param {} fontStr 待解析字符串的font(..) * @returns 解析完css字体样式 /function fontParse(fontStr) { let start = fontStr.indexOf(’(’) + 1 // ‘(’后面一个字符位置 let args = fontStr.slice(start, -1) // 就是取font(..)括号里面字符串,按例子args为'20px, yellow’ let argsList = args.split(’,’) // 分割成数组[‘20px’, ‘yellow’] switch (argsList.length) { // 判断开发者传递参数个数 case 2: argsList.push(’left’) break case 1: argsList.push(’#000’, ’left’) break } cssName = [‘font-size’, ‘color’, ’text-align’].map((item, index) => { return ${item}:${argsList[index]} // 组合成新的样式数组 }) let cssStr = cssName.join(’;’) //形成新css字符串 return cssStr // 回调}module.exports = function (content) { // content就是在<style>..</style>写的里面的内容,或者引入的.css文件 content = content.replace(/font((.|\n)?)/ig, (str) => fontParse(str)) this.cacheable() // 缓存 this.callback(null, content) // 回调}上面的正则会匹配到font(..), str就匹配到的font(20px, yellow)字符串, 最终会被fontParse(str)值替换.就已经完成font的解析其它样式的解析可以写成链式module.exports = function (content) { // content就是在<style>..</style>写的里面的内容,或者引入的.css文件 content = content.replace(/font((.|\n)?)/ig, (str) => fontParse(str)) // 解析font()字体样式 .replace(/wh((.|\n)?)/ig, (str) => whParse(str)) // 解析wh()宽高样式 .replace(/posC(.|\n)?;/ig, (str) => posCParse(str)) // 解析posC居中样式 .replace(/flex(.|\n)?;/ig, (str) => flexParse(str)) // 解析flex布局样式 .replace(/comB(.|\n)?;/ig, (str) => backParse(str)) // 解析comB(..)背景图片样式 .replace(/posL(.|\n)?;/ig, (str) => posLRParse(str, ’left’)) // 解析posL(..)居左样式 .replace(/posR(.|\n)?;/ig, (str) => posLRParse(str, ‘right’)) // 解析posR(..)居右样式 this.cacheable() // 缓存 this.callback(null, content) // 回调}一个loader大功告成说明具体的可以去看源码,在github上,觉得可以的话帮忙star一下我发布npm包easy-css-loader使用如下配置{ module: { rules: [ { test: /.css$/, use: [‘style-loader’, ‘css-loader’, ’easy-css-loader’] // 顺序一定要注意 } ] }} ...

January 25, 2019 · 2 min · jiezi

vue组件文档(.md)中自动导入示例(.vue)

症结(懒癌患者)在写组件库文档的时候,会把示例代码粘贴到文档里,这样做有一个很恶心的地方:每次组件迭代或修改示例都需要重新修改文档中的代码片段。长年累月,苦不堪言。猜想(狂想曲)所以我想,可不可以把.vue文件里的template块和script块取出来,放入对应的.md文件中比如在.md文件中 {{:xx.vue?type=(template|script)}} 便替换示例中对应的template|script块# xx## 示例代码// {{:}} 定义变量规则模版(加个冒号防冲突){{:image.vue?type=template}} // 对应.vue 的template{{:image.vue?type=script}} // 对应.vue 的template{{:index.js}} // 对应index.js## 参数说明xxx…output# xx## 示例代码 // image.vue template<template> <div>xx</div></template>// image.vue script<script> …</script>// index.jsvar x = 1## 参数说明xxx…动手(能动手绝不**)要实现以上功能,需要探索以下几点:从.vue里取出template&script塞进对应的.md的变量位置将.md文件转为Vue Componet / html如果按照我们写js的习惯,以下嵌套排列可能更易读将.md文件转为Vue Componet / html找到变量位置,塞进对应的.md的指定位置从.vue里取出template&script一步一步来吧:1、将.md文件转为Vue Componet / html要想在vue中使用.md文件为组件,只需要用loader将md转成Vue Componet即可。这个过程很简单,以下为loader伪代码const wrapper = content => &lt;template&gt; &lt;section v-html="content" v-once /&gt;&lt;/template&gt;&lt;script&gt;export default { created() { this.content = content }};&lt;/script&gt;module.exports = function(source) { // markdown 编译用的 markdown-it return wrapper(new MarkdownIt().render(source)) }2、找到变量位置,塞进对应的.md的指定位置1)找到变量位置使用正则匹配定义的规则,找到被{{:}} 包围的字符串,如上例所示则为‘image.vue?type=template’2)读取文件如果是其他.js、.html等普通文件,直接使用fs.readFileSync读取替换即可,因是.vue,我们希望传入type来获取不同的块(template、script等)const replaceResults = (template, baseDir) => { const regexp = new RegExp("\{\{:([^\}]+)\}\}", “g”) return template.replace(regexp, function(match) { // 获取文件变量 match = match.substr(3, match.length - 5) let [loadFile, query=’’] = match.split(’?’) // 读取文件内容 const source = fs.readFileSync(path.join(baseDir, loadFile), “utf-8”).replace(/[\r\n]*$/, “”) if (path.extname(loadFile) === “.vue”) { let { type } = loaderUtils.parseQuery(?${query}) return replaceVue(source, type) // 根据type提取.vue里的不同块 } return source // 非.vue直接返回文件内容 })};3、从.vue里取出template&scriptconst replaceVue = (source, type) => { const descriptor = templateCompiler.parseComponent(source) const lang = { template: ‘html’, script: ‘javascript’ //, // style: ‘css’ } return lang[type] && \``${lang[type]} ${descriptor[type].content} ``` }如若要取一个文件里的多个块,则需多次调用,考虑到我们的组件库场景,默认返回template和script(未使用type参数时),对上面代码进行优化,一次性从.vue中取出多个块// replaceVue(source, [type])const replaceVue = (source, types = ['template', 'script']) =&gt; { const descriptor = templateCompiler.parseComponent(source) const lang = { template: 'html', script: 'javascript' //, // style: 'css' } return types.map(type =&gt; lang[type] &amp;&amp; ```${lang[type]} ${descriptor[type].content} ``` `).join(’’)}大功告成????????! 那么,如何使用呢?使用(给我小星星????)安装npm i vue-markd-loader -D配置rules: [{ test: /.md$/, use: [ ‘vue-loader’, { loader: ‘vue-markd-loader’, options: { replaceFiles: true , // 默认true, 是否将文件填充进md wrapper:true // 默认true,默认输出Vue Component ,false 时输出html片段 } } ] }]// main.js import ‘highlight.js/styles/github.css’ // 可以使用任意喜欢的主题哟import ‘github-markdown-css’其他第一次撸webpack loader,如有不正确/不足的地方,欢迎指正。源代码git地址npm包地址 ...

January 25, 2019 · 2 min · jiezi

收集vue2精佳文章,一月下半月 - 岂敢定居,一月三捷

一月: 一年之计在于春,一月是一年的开始一月下半月-岂敢定居,一月三捷。(01.16~01.31):寄徐丞 [宋] 赵师秀 病不窥园经一月,更无人迹损青苔。 池禽引子衡鱼去,野蔓开花上竹来。 亦欲鬓毛休似雪,争如丹汞只为灰。 秋风昨夜吹寒雨,有梦南游到海回。文章列表【收藏】2019年最新Vue相关精品开源项目库汇总创建vue-cli框架项目VuePress博客搭建笔记(二)个性化配置【前端笔记】Vuex快速使用vue -on如何绑定多个事件基于vue的验证码组件Vue自定义Toast插件Vue添加数据视图不更新问题聊一聊Vue组件模版,你知道它有几种定义方式吗?深入学习Vue SSR服务端渲染 用Nuxt.js打造CNode社区vue 源码学习(二) 实例初始化和挂载过程使用NodeJS 生成Vue中文版 docSet 离线文档手牵手教你写 Vue 插件Vue项目部署遇到的问题及解决方案预计今年发布的 Vue 3.0 到底有什么不一样的地方?记一次 Vue 单页面上线方案的优化vue-cli3使用svg问题的简单解决办法从react转职到vue开发的项目准备基于 Vue-Cli3 构建的脚手架模版新手福音用vue-cli3从0到1做一个完整功能手机站(一)vue-cli3 从搭建到优化Spring Security (三):与Vue.js整合电商网站项目总结:Vuex 带来全新的编程体验结合vue-cli来谈webpack打包优化vue开发环境配置跨域,一步到位Vue新手向:14篇教程带你从零撸一个Todo应用Vue2.0 核心之响应式流程基于Vue的任务节点图绘制插件(vue-task-node)Vue 实践小结vue项目接口管理【vue-cli3升级】老项目提速50%(二)Vuex 是什么,为什么需要【收藏】2019年最新Vue相关精品开源项目库汇总创建vue-cli框架项目VuePress博客搭建笔记(二)个性化配置RFCs for substantial changes / feature additions to Vue core 3.0February 14-15 - Vue.js AmsterdamMarch 25-27 - VueConf US in Tampa, FLApril 12th, VueDay 2019, Verona, Italy – Call for papers until 1st FebruaryIncoming workshop: Proven patterns for building Vue apps by Chris Fritz10 Best Tips for Learning Vue from Vue MastersImprove performance on large lists in VueBuild a Beautiful Website with VuePress and Tailwind.cssSelectively enable SSR or SPA mode in a Nuxt.js appGive Users Control Over App Updates in Vue CLI 3 PWAsWhen to “componentize” from the point of VueVue Route Component HooksWatch for Vuex State changes!GitHub - egoist/styled-vueGitHub - Justineo/vue-clampGitHub - edisdev/vue-datepicker-uiVue2+周报不积跬步,无以至千里;不积小流,无以成江海丁酉年【鸡年】/戊戌年【狗年】/己亥年【猪年】 对【Vue相关开源项目库汇总】的Star更新排名几个值得收藏的国外有关Vue.js网站(https://segmentfault.com/a/11… :conf.vuejs.org国外一个Vue.js视频教程scotch网站的技术视频教程vue-hackernews-2.0Weekly dose of handpicked Vue.js news!vuejsdevelopers-Vue开发者网站还是个人的?vuejsfeed-最新的Vue.js新闻、教程、插件等等vuecomponents-Vue.js组件集合社区madewithvuejs-收藏了用Vue.js实现的网站vuejsexamples-Vue.js的Demo满满的最新vueNYC资讯:VueNYCVueNYC - Vue.js: the Progressive Framework - Evan Youtwitter-search-VueNYC尤大大的PPT我已经上传了../PPT/Vue.js the Progressive Framework.pdf最新2018 VueConf资讯: (第二届VueConf将于2018年11月24日在杭州举行) ::资料:: ::PPT::更多资讯集web前端最近很火的vue2框架资源;定时更新,欢迎 Star 一下。:::【点击我】::: ...

January 25, 2019 · 1 min · jiezi

vscode中的 jsconfig.json

问题源头:webpack模板里import路径中@符号是什么意思?什么是jsconfig.json?目录中存在jsconfig.json文件表示该目录是JavaScript项目的根目录。jsconfig.json文件指定根文件和JavaScript语言服务提供的功能选项。提示:如果您不使用JavaScript,则无需担心jsconfig.json。提示:jsconfig.json源于tsconfig.json,是TypeScript的配置文件。jsconfig.json相当于tsconfig.json的“allowJs”属性设置为true。为什么我需要一个jsconfig.json文件?Visual Studio Code的JavaScript支持可以在两种不同的模式下运行:文件范围 - 没有jsconfig.json:在此模式下,在Visual Studio Code中打开的JavaScript文件被视为独立单元。 只要文件a.js没有显式引用文件b.ts(使用///引用指令或CommonJS模块),两个文件之间就没有共同的项目上下文。显式项目 - 使用jsconfig.json:JavaScript项目是通过jsconfig.json文件定义的。 目录中存在此类文件表示该目录是JavaScript项目的根目录。 文件本身可以选择列出属于项目的文件,要从项目中排除的文件,以及编译器选项(见下文)。当您在工作空间中有一个定义项目上下文的jsconfig.json文件时,JavaScript体验会得到改进。 因此,当您在新工作空间中打开JavaScript文件时,我们提供了一个创建jsconfig.json文件的提示。jsconfig.json的位置我们通过创建jsconfig.json文件将我们代码的这一部分(我们网站的客户端)定义为JavaScript项目。 将文件放在JavaScript代码的根目录下,如下所示。在更复杂的项目中,您可能在工作空间中定义了多个jsconfig.json文件。 您将需要执行此操作,以便不将一个项目中的代码建议为IntelliSense以在另一个项目中进行编码。 下面的插图是一个带有客户端和服务器文件夹的项目,显示了两个单独的JavaScript项目。例子默认情况下,JavaScript语言服务将分析并为JavaScript项目中的所有文件提供IntelliSense(智能感知)。 您需要指定要排除或包含的文件,以便提供正确的IntelliSense。使用“exclude”属性exclude属性(glob模式)告诉语言服务哪些文件是什么文件,而不是源代码的一部分。 这使性能保持在较高水平。 如果IntelliSense速度很慢,请将文件夹添加到排除列表中(如果检测到速度减慢,VS代码将提示您执行此操作)。{ “compilerOptions”: { “target”: “es6” }, “exclude”: [ “node_modules” ]}提示:您将要排除由构建过程生成的文件(例如,dist目录)。 这些文件会导致建议显示两次,并会降低IntelliSense的速度。使用“包含”属性或者,可以使用include属性(glob模式)显式设置项目中的文件。如果不存在include属性,则默认为包含目录和子目录中的所有文件。如果指定了include属性,则只包括这些文件。下面是一个具有显式include属性的示例。{ “compilerOptions”: { “target”: “es6” }, “include”: [ “src/**/” ]}提示:exclude和include中的文件路径是相对于jsconfig.json的位置。jsconfig选项下面是jsconfig“compilerOptions”来配置JavaScript语言支持。提示:不要被compilerOptions混淆。 此属性的存在是因为jsconfig.json是tsconfig.json的后代,后者用于编译TypeScript。选项描述类型默认noLib不包含默认库文件(lib.d.ts)string-target指定要使用的默认库(lib.d.ts)。 值是“es3”,“es5”,“es6”,“es2015”,“es2016”,“es2017”,“es2018”,“esnext”。string-checkJs启用JavaScript文件的类型检查。booleantrueexperimentalDecorators为拟议的ES装饰器提供实验支持。string-allowSyntheticDefaultImports允许从没有默认导出的模块进行默认导入。 这不会影响代码发出,只会影响类型检查。booleantruebaseUrl用于解析非相对模块名称的基目录。string-paths指定相对于baseUrl选项计算的路径映射。object见demo使用webpack别名要使IntelliSense使用webpack别名,您需要使用glob模式指定paths键。例如,对于别名’ClientApp’(或@):{ “compilerOptions”: { “baseUrl”: “.”, “paths”: { “ClientApp/”: ["./ClientApp/*"] } }}然后使用别名import Something from ‘ClientApp/foo’// 或 import Something from ‘@/foo’最佳实践只要有可能,您应该使用不属于项目源代码的JavaScript文件排除文件夹。提示:如果工作空间中没有jsconfig.json,则默认情况下,VS Code将排除node_modules文件夹。下面是一个将常见项目组件映射到其安装文件夹的表,建议将其排除在外:组件要排除的文件夹node排除node_modules文件夹webpack, webpack-dev-server排除内容文件夹,例如dist。bower排除bower_components文件夹ember排除tmp和temp文件夹jspm排除jspm_packages文件夹当您的JavaScript项目变得太大而性能变慢时,通常是因为像node_modules这样的库文件夹。 如果VS Code检测到您的项目变得过大,则会提示您编辑排除列表。提示:有时无法正确选择更改配置,例如添加或编辑jsconfig.json文件。 运行Reload JavaScript Project命令应重新加载项目并获取更改。使用TypeScript编译器进行低级编译当tsc用于将ES6 JavaScript向下级编译为旧版本时,jsconfig.json中的以下编译器选项适用:选项描述module指定模块代码生成。 值为“commonjs”,“system”,“umd”,“amd”,“es6”,“es2015”diagnostics显示诊断信息。emitBOM在输出文件的开头发出UTF-8字节顺序标记(BOM)。inlineSourceMap使用源映射发出单个文件,而不是使用单独的文件。inlineSources在单个文件中将源与源图一起发出; 需要设置–inlineSourceMap。jsx指定JSX代码生成:“保留”或“反应”?。reactNamespace指定在针对’react’JSX发出的目标时为createElement和__spread调用的对象。mapRoot将位置指定为字符串中的uri,其中调试器应该找到映射文件而不是生成的位置。noEmit不发起输出。noEmitHelpers不在编译输出中生成自定义辅助函数,如__extends。noEmitOnError如果报告任何类型检查错误,不发起输出。noResolve不将三斜杠引用或模块导入目标解析为输入文件。outFile连接并将输出发送到单个文件。outDir将输出结构重定向到目录。removeComments不向输出发出注释。rootDir指定输入文件的根目录。用于通过–outDir控制输出目录结构。sourceMap生成相应的’.map’文件。sourceRoot指定调试器应该找到JavaScript文件而不是源位置的位置。stripInternal不为具有’@internal’注释的代码发出声明。watch监听输入文件。emitDecoratorMetadata在源中为装饰声明发出设计类型元数据。noImplicitUseStrict不在模块输出中发出“use strict”指令。这个文档有用么?参考:JavaScript语言服务TypeScript tsconfig.jsonglob模式glob (programming))webpack模板里import路径中@符号是什么意思?翻译:Xindot原文:https://code.visualstudio.com… ...

January 24, 2019 · 1 min · jiezi

webpack4使用笔记之plugin

webpack4pluginclean-webpack-pluginhtml-webpack-pluginmini-css-extract-pluginterser-webpack-plugin(webpack4内置)optimize-css-assets-webpack-pluginwebpack-bundle-analyzerclean-webpack-plugin清除文件插件new cleanWebpackPlugin( [resolve(‘dist’), resolve(‘build’)], { root: __dirname, exclude: [’test.html’], // 排除 verbose: true, // Write logs to console. dry: false, // Use boolean “true” to test/emulate delete. (will not remove files). watch: false, // If true, remove files on recompile. beforeEmit: false // 在将文件发送到输出目录之前执行清理})html-webpack-plugin自动帮你生成一个 html 文件,并且引用相关的 assets 文件(如 css, js)。如果你既指定了 template 选项,又指定了 title 选项,那么webpack 会选择哪一个? 事实上,这时候会选择你指定的模板文件的 title, 即使你的模板文件中未设置 title。new htmlWebpackPlugin({ title: ’testooo’, // html 文件的标题 filename: ‘detail.html’, // 生成 html 文件的文件名。默认为 index.html. template: ./src/detail.html, // 根据自己的指定的模板文件来生成特定的 html 文件 chunks: [‘index’, ‘vendor’], // 主要是针对多入口(entry)文件。当你有多个入口文件的时候,对应就会生成多个编译后的 js 文件。那么 chunks 选项就可以决定是否都使用这些生成的 js 文件。默认引用所有 inject: ‘body’, // true默认值,script标签位于html文件的 body 底部 注入选项 hash: true, // hash选项的作用是 给生成的 js 文件一个独特的 hash 值 xx.js?xxxxxx // 对 html 文件进行压缩 minify: { removeAttributeQuotes:true, removeComments: true, collapseWhitespace: true, removeScriptTypeAttributes:true, removeStyleLinkTypeAttributes:true }})mini-css-extract-plugin分离css: 此插件将CSS提取到单独的文件中。它为每个包含CSS的JS文件创建一个CSS文件。它支持CSS和SourceMaps的按需加载。extract-text-webpack-plugin ,只支持 webpack 4 以下提取 CSS 文件, webpack 4以上用mini-css-extract-pluginterser-webpack-plugin(webpack4内置)webpack 4 中删除了 webpack.optimize.CommonsChunkPlugin,并且使用 optimization 中的splitChunk来替代,下面的配置代替了之前的 CommonsChunkPlugin 配置,同意能提取 JS 和 CSS 文件压缩的配置也移动到了 optimization 选项下,值得注意的是压缩工具换成了 terser-webpack-plugin,这是 webpack 官方也推荐使用的module.exports = { optimization: { splitChunks: { vendors: { name: ‘venders’, chunks: ‘all’, minChunks: chunks.length } }}optimize-css-assets-webpack-pluginwebpack5很可能会内置css压缩,为了压缩输出,可使用optimize-css-assets-webpack-plugin插件。默认只压缩js,所以通过设置optimization.minimizer覆盖默认的压缩配置,应确保指定了一个JS的压缩配置, 否则默认配置就被覆盖了,就不再压缩js了。类似extract-text-webpack-plugin,通过optimization.splitChunks.cacheGroups可以将css提取到一个文件中。module.exports = { minimizer: [ new terserPlugin({ // 压缩js cache: true, parallel: true }), new optimizeCSSAssetsPlugin({ // 压缩css cssProcessorOptions: { safe: true } }) ], optimization: { cacheGroups: { styles: { name: ‘styles’, test: /.scss|css$/, chunks: ‘all’, // merge all the css chunk to one file enforce: true } } }}webpack-bundle-analyzer通过使用webpack-bundle-analyzer可以看到项目各模块的大小,可以按需优化图片来自于webpack-bundle-analyzer的github ...

January 24, 2019 · 1 min · jiezi

Webpack 4.0 CommonsChunkPlugin 和 optimization splitChunks

该文章内容大致翻译自 webpack 4: Code Splitting, chunk graph and the splitChunks optimization原有的问题webpack 4.0 对代码模块的关系图进行了一些巨大的优化,同时添加了一个新的 optimization 用于模块的分离(可以看做是对 CommonsChunkPlugin 的一次优化)。先让我们看看旧版关系图的一些缺陷。在之前的版本中,我们将各个模块打包进编译后的文件之中,同时这些文件之间又是通过父子关系来进行关联,最后将我们整个项目中的所有模块串联起来。当其中一个文件含有父级引用,那我们可以推断出,在该文件完成加载时,已经成功加载了父级文件。那么我们可以据此进行一些优化。比如当一个文件中的模块已经在父级文件中正常运行,那么我们可以将该模块从文件中移除,因为它必然已经被成功加载。在入口点或异步拆分点处引用这些文件。这些文件将会并行加载。这种类型的关系图使得分离splitting变得非常困难。比如在你使用CommonsChunkPlugin插件时。会有一个或多个文件内模块被移动到新的文件中来。这一整个新文件需要被添加到关系图中来。但我们应该如何设置它的层级呢?作为旧有文件的父级?还是子级?CommonsChunkPlugin中将其设置为父级,但从技术层面来说,这是错误的,并且导致了一些负面的优化结果(提前加载的这个文件中的部分模块不是必需的)。现在新的关系图中,引入了一个新概念:ChunkGroup。包含文件列表的文件分组。在入口点或异步拆分点处我们会引用这个文件分组,该分组内的文件全都是并行加载。而一个文件可以被多个文件分组引用(但不会多次加载)。现在不再使用父子级的关系来描述文件之间的联系,取而代之的是文件分组ChunkGroup。那么此时,splitting文件就能够被表述出来。被创建的新文件可以被添加到所有包含原始文件的文件分组中。同时也不会因此产生负面优化效果。CommonsChunkPlugin和SplitChunksPlugin的区别这个问题被修复之后,我们可以更多的使用文件拆分了。我们可以将任何文件拆分出来并且不需要提高其加载优先级。CommonsChunkPlugin存在以下这些问题:需要下载当前还不需要使用的代码文件。异步加载使用文件效率低下。很难使用。(猜测这里指的是配置)实现方式很难理解。所以新的插件诞生了:SplitChunksPlugin。它会使用模块引用计数和模块类别区分(比如:node_modules)来自动分离出需要被拆分的文件内引用模块。There is a paradigm shift here. The CommonsChunkPlugin was like: “Create this chunk and move all modules matching minChunks into the new chunk”. The SplitChunksPlugin is like: “Here are the heuristics, make sure you fullfil them”. (imperative vs declarative)这里没有理解全部的内容。SplitChunksPlugin同时提供了更多的特性:不会加载非必须文件(除非进行了强制合并)异步文件处理更有效率。默认异步处理文件。它将引用模块分散到多个库文件中。更容易使用。不依赖文件引用关系图。更加的自动化。例子下面是一些使用SplitChunksPlugin的例子。这些用例仅仅展现了它在默认配置下的行为。你也可以使用额外配置项来进行个性化定制。提示:可以通过optimization.splitChunks进行配置。这里的例子是关于文件的,默认情况下仅适用于异步加载的文件块。但也可以添加optimization.splitChunks.chunks: “all"来配置适用于所有类型的文件。我们假定每个额外导入的库文件都大于30kb,因为优化仅在该体积之后开始进行。(可以通过配置minSize属性进行修改,默认 30000)Vendorschunk-a: react, react-dom, some componentschunk-b: react, react-dom, some other componentschunk-c: angular, some componentschunk-d: angular, some other componentswebpack将会自动创建2个库文件:vendorschunk-achunk-b: react, react-domvendorschunk-cchunk-d: angularchunk-a chunk-b chunk-c chunk-d: 仅含有componentsVendors overlappingchunk-a: react, react-dom, some componentschunk-b: react, react-dom, lodash, some other componentschunk-c: react, react-dom, lodash, some componentswebpack依然会创建2个库文件:vendorschunk-achunk-bchunk-c: react, react-domvendorschunk-bchunk-c: lodashchunk-a chunk-b chunk-c: 仅含有componentsShared moduleschunk-a: vue, some components, some shared componentschunk-b: vue, some other components, some shared componentschunk-c: vue, some more components, some shared components假设所有的shared components体积都大于 30kb,webpack将会创建一个库文件和一个通用组件文件:vendorschunk-achunk-bchunk-c: vuecommonschunk-achunk-bchunk-c: some shared componentschunk-a chunk-b chunk-c: 仅含有components当这些shared components体积小于30kb是,webpack会故意将该模块复制到chunk-a chunk-b chunk-c三个文件中。我们认为进行分离所减小的加载体积的整体效果并不如一次额外的加载请求的消耗。Multiple shared moduleschunk-a: react, react-dom, some components, some shared react componentschunk-b: react, react-dom, angular, some other componentschunk-c: react, react-dom, angular, some components, some shared react components, some shared angular componentschunk-d: angular, some other components, some shared angular componentswebpack将会创建2个库文件及2个通用组件文件vendorschunk-achunk-bchunk-c: react, react-domvendorschunk-bchunk-cchunk-d: angularcommonschunk-achunk-c: some shared react componentscommonschunk-c~chunk-d: some shared angular componentschunk-a chunk-b chunk-c chunk-d: Only the components提示:因为生成的导入文件名称包含所有的原始文件名称,所以我们推荐在生产环境中使用的长效缓存文件不要包含[name]在文件名中,或者设置optimization.splitChunks.name: false来关闭文件名生成逻辑。这样即使在后续开发中对该文件添加了新的引用,也不会修改文件名,该缓存逻辑依然生效。 ...

January 23, 2019 · 1 min · jiezi

Vue项目build后,图片加载不出来

vue项目,build之后会对图片进行处理,具体处理的方式是在文件webpack.base.conf.js中,有如下代码:module: { rules: [ { test: /.(png|jpe?g|gif|svg)(?.*)?$/, loader: ‘url-loader’, options: { limit: 10000, // 1k—–限制文件的大小 name: utils.assetsPath(‘img/[name].[hash:7].[ext]’) } } ]}以上代码中,使用url-loader对图片的大小进行限制,在limit之内,webpack会将图片转化为base64,超出limit限制,交给file-loader处理。如果在limit范围之内,不会出现图片加载不出来的情况;超出limit,webpack会将处理后的图片放在dist/static/img/中,此时加载图片将会显示不出来。具体做法如下:1、在config/index.js文件内,修改代码: (列出index.js的部分代码)build: { // Template for index.html index: path.resolve(__dirname, ‘../dist/index.html’), // Paths assetsRoot: path.resolve(__dirname, ‘../dist’), assetsSubDirectory: ‘static’, assetsPublicPath: ‘./’, }assetsPublicPath字段值由之前的’/‘改为’./’;2、在webpack.prod.conf.js文件内,output字段,添加代码(publicPath: ‘./’):output: { publicPath: ‘./’, // 添加的代码 path: config.build.assetsRoot, filename: utils.assetsPath(‘js/[name].[chunkhash].js’), chunkFilename: utils.assetsPath(‘js/[id].[chunkhash].js’) },3、在utils.js文件里添加 publicPath:’../../’,代码如下:// (which is the case during production build)if (options.extract) { return ExtractTextPlugin.extract({ use: loaders, fallback: ‘vue-style-loader’, publicPath: ‘../../’ // 添加的代码 })} else { return [‘vue-style-loader’].concat(loaders)}以上步骤操作完后,执行命令:npm run build在build后,dist中的index.html页面的link、script标签的引入路径变为相对路径;同时,相关的图片路径,也变变为相对路径,此时部署项目,不再出现图片路径404。 ...

January 22, 2019 · 1 min · jiezi

为什么自己写的组件库被引用总是报错——详解webpack的library和libraryTarget

如果我们仅仅是实现一个项目,我们大概率不会关注到webpack output中的这两个属性。但是如果我们是实现一个组件库,那么这两个属性就变得至关重要了。本文从自己之前遇到的一个问题说起,继而引申出library和libraryTarget属性。1. 故事起源当我自己开始写第一个组件库的时候,很快我就撸好了框架的代码,然后我兴致冲冲的把我的组件库引入到我的项目中,我记得那时候我是这么写的:组件库:import Feeds from ‘@/components/feeds/index’;export { Feeds,};主项目:import Feeds from ‘@/tencent/newsH5Ad’;// 一些其他代码<Feeds data=‘xxx’>然后我就收获了一个报错,Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: null。啊?难道是我的最终输出代码有问题?我检查了一下最终输出的代码,没有问题,Feed组件的代码也在里面。这个问题我查了很久,都没有答案,最后才发现是webpack打包的问题。这就涉及到了本文的主角,library和libraryTarget。2.2. library和libraryTarget我们都知道,webpack可以将不同的模块化方式(commonjs, AMD, CMD, ES6 Module)的代码打包。那我们打出来的代码包其实也可以按不同的模块化方式生成,所以:libraryTarget就是配置webpack打包内容的模块方式的参数而library就是webpack打包内容的名字所以library规定了组件库返回值的名字,libraryTarget规定了返回值的编码格式。libraryTarget的配置选项可以分为四大类:2.1 按不同的模块方式生成也就是我们这个问题的解决方法,由于我写的是一个React的UI组件库,所以我们需要commonjs的模块方式。因此只需要在webpack.config.js中配置这一项即可:module.exports = { entry: ‘./src/index.js’, output: { filename: ‘index.js’, // library: ‘MyLibrary’, // 模块名称 libraryTarget: ‘commonjs2’, // 输出格式 }, // 其他代码}事实上,你可以选择的选项有:commonjs/commonjs2: 将你的library暴露为CommonJS模块amd: 将你的library暴露为amd模块umd: 将你的library暴露为所有的模块定义下都可运行的方式其中AMD和UMD需要指定library,如果不声明组件库则不能正常运行。这是为了在浏览器上通过script标签加载时,用AMD模块方式输出的组件库可以有明确的模块名。如:define(“MyLibrary”, [], function() { return entry_return; // 此模块返回值,是入口 chunk 返回的值});注意:commonjs和commonjs2几乎相同,只不过commonjs只包含exports,而commonjs2还包含module.exports,所以直接使用commonjs2即可。2.2 生成为一个变量libraryTarget的默认值是var,顾名思义,就是将组件库入口起点的返回值生成一个变量。如:var MyLibrary = entry_return;也可以选择‘assign’,那样的话将默认生成和一个全局的变量。不管是var还是assign,都需要设置library的名称,否则就会报错。2.3 生成一个为一个对象的属性和第二种情况差不多,只不过会把这个变量赋值给某个对象,作为它的一个属性存在。可以选择的选项有:this: 返回值成为this的一个属性window: 返回值成为window的一个属性global: 返回值成为global的一个属性例如:this[“MyLibrary”] = entry_return;window[“MyLibrary”] = entry_return;global[“MyLibrary”] = entry_return;可以看到,这种情况下也必须指定library的名字。2.4 异步生成方式在这种情况下,libraryTarget的值为‘jsonp’,组件库入口起点的返回值,会被包裹到一个jsonp包装容器中,并配合webpack的externals使用——组件库的依赖由externals指定。如:MyLibrary(entry_return);3. 总结本文介绍了webpack中libraray和libraryTarget的相关内容,解释了为什么不设置它们时使用webpack打包出来的组件库会有问题。一般情况下,作为vue或者react组件库,libraryTarget在commonjs2,amd,umd中三者择其一即可。参考文献《webpack文档》 ...

January 21, 2019 · 1 min · jiezi

webpack4初学习

webpack基于node,因此想要学习webpack首先要安装node。webpack4要安装node8.2以上版本。1、安装webpack全局安装,方便以后使用npm install webpack webpack-cli -ga. 或者本地安装mkdir webpack-testcd webpack-testnpm init //初始化npm,文件夹多出package.jsonnpm i webpack webpack-cli -D★ npm i -D 是 npm install –save-dev 的简写,是指安装模块并保存到 package.json 的 devDependencies中,主要在开发环境中的依赖包b. 根目录下创建main.js文件document.write(‘Hello world!’)c. 初试webpackwebpack main.js -o bundle.js // 打包main.js文件,包文件bundle.js未完,待续

January 21, 2019 · 1 min · jiezi

create-react-app 2.0中使用antd(eject)

早些时候CRA(create-react-app)升级到2.0.3的时候, react-app-rewired没有跟着升级, 导致项目无法启动, 于是乎直接eject 开始改造项目.查看版本> create-react-app –version2.0.3创建项目create-react-app my-projectcd my-projectyarn eject # 输入 y目前为止项目目录结构, 忽略node_modules这个黑洞├── README.md├── config│ ├── env.js│ ├── jest│ │ ├── cssTransform.js│ │ └── fileTransform.js│ ├── paths.js│ ├── webpack.config.js│ └── webpackDevServer.config.js├── package.json├── public│ ├── favicon.ico│ ├── index.html│ └── manifest.json├── scripts│ ├── build.js│ ├── start.js│ └── test.js├── src│ ├── App.css│ ├── App.js│ ├── App.test.js│ ├── index.css│ ├── index.js│ ├── logo.svg│ └── serviceWorker.js└── yarn.lock安装依赖yarn add antdyarn add babel-plugin-import less less-loader @babel/plugin-proposal-decorators -DCRA eject之后package.json里面没有区分devDependencies 和 dependencies, 但是不影响使用因为antd是使用的less, CRA默认不支持, 所以需要改下默认的webpack配置, config/webpack.config.js首先修改babel配置个人习惯使用babelrc, 所以把babel-loader options中babelrc的值改为true, 增加.babelrc文件{ “presets”: [ “react-app” ], “plugins”: [ [ “import”, { “libraryName”: “antd”, “libraryDirectory”: “lib”, “style”: true }, “ant” ], [ “@babel/plugin-proposal-decorators”, // 启用装饰器 { “legacy”: true } ] ]}参照默认的sass配置, 增加less配置const sassRegex = /.(scss|sass)$/;const sassModuleRegex = /.module.(scss|sass)$/;const lessRegex = /.less$/;const lessModuleRegex = /.module.less$/;在module>rules中添加规则// sass rule//… { test: lessRegex, exclude: lessModuleRegex, use: getStyleLoaders( { importLoaders: 2, sourceMap: isEnvProduction && shouldUseSourceMap }, ’less-loader’, { javascriptEnabled: true } ), sideEffects: true }, { test: lessModuleRegex, use: getStyleLoaders( { importLoaders: 2, sourceMap: isEnvProduction && shouldUseSourceMap, modules: true, getLocalIdent: getCSSModuleLocalIdent }, ’less-loader’, { javascriptEnabled: true } ) }// file loader至此基本项目虽然已经基本完成, 但是如果你是使用less版本比较高, 项目是无法运行的参考issue需要改造getStyleLoaders函数, 增加第三个参数otherConfig, 就是上面代码中的 javascriptEnabled: trueconst getStyleLoaders = (cssOptions, preProcessor, otherConfig) => { const loaders = [ isEnvDevelopment && require.resolve(‘style-loader’), isEnvProduction && { loader: MiniCssExtractPlugin.loader, options: Object.assign({}, shouldUseRelativeAssetPaths ? { publicPath: ‘../../’ } : undefined) }, { loader: require.resolve(‘css-loader’), options: cssOptions }, { loader: require.resolve(‘postcss-loader’), options: { ident: ‘postcss’, plugins: () => [ require(‘postcss-flexbugs-fixes’), require(‘postcss-preset-env’)({ autoprefixer: { flexbox: ’no-2009’ }, stage: 3 }) ], sourceMap: isEnvProduction && shouldUseSourceMap } } ].filter(Boolean); if (preProcessor) { loaders.push({ loader: require.resolve(preProcessor), options: { sourceMap: isEnvProduction && shouldUseSourceMap, …otherConfig } }); } return loaders; };这样修改之后, 自定义主题modifyVars也可以写在otherConfig中, 一举两得, 不多赘述.至此项目???? ...

January 20, 2019 · 2 min · jiezi

webpack构建和性能优化探索

前言随着业务复杂度的不断的增加,工程模块的体积也会不断增加,构建后的模块通常要以M为单位计算。在构建过程中,基于nodejs的webpack在单进程的情况下loader表现变得越来越慢,在不做任何特殊处理的情况下,构建完后的多项目之间公用基础资源存在重复打包,基础库代码复用率也不高,这都慢慢暴露出webpack的问题。正文针对存在的问题,社区涌出了各种解决方案,包括webpack自身也在不断优化。构建优化下面利用相关的方案对实际项目一步一步进行构建优化,提升我们的编译速度,本次优化相关属性如下:机器: Macbook Air 四核 8G内存Webpack: v4.10.2项目:922个模块构建优化方案如下:减少编译体积大小将大型库外链将库预先编译使用缓存并行编译初始构建时间如下:增量构建Development 构建Production 构建备注3088ms43702ms89371ms 减少编译体积大小初始构建时候,我们利用webpack-bundle-analyzer对编译结果进行分析,结果如下:可以看到,td-ui(类似于antd的ui组件库)、moment库的locale、BizCharts占了项目的大部分体积,而在没有全部使用这些库的全部内容的情况下,我们可以对齐进行按需加载。针对td-ui和BizCharts,我们对齐添加按需加载babel-plugin-import,这个包可以在使用ES6模块导入的时候,对其进行分析,解析成引入相应文件夹下面的模块,如下:首先,我们先添加babel的配置,在plugins中加入babel-plugin-import:{ … “plugins”: [ … [“import”, [ { libraryName: ’td-ui’, style: true }, { libraryName: ‘bizcharts’, libraryDirectory: ’lib/components’ }, ]] ]}可以看到,我们给bizcharts也添加了按需加载,配置中添加了按需加载的指定文件夹,针对bizcharts,编译前后代码对比如下:编译前:编译后:注意:bizcharts按需加载需要引入其核心代码bizcharts/lib/core;到此为止,td-ui和bizcharts的按需加载已经处理完毕,接下来是针对moment的处理。moment的主要体积来源于locale国际化文件夹,由于项目中有中英文国际化的需求,我们这里使用webpack.ContextReplacementPugin对该文件夹的上下文进行匹配,只匹配中文和英文的语言包,plugin配置如下:new webpack.ContextReplacementPugin( /moment[/\]locale$/, //匹配文件夹 /zh-cn|en-us/ // 中英文语言包)如果没有国际化的需求,可以使用webpack.IgnorePlugin对整个locale文件夹进行忽略,配置如下:new webpack.IgnorePlugin(/^./locale$/, /moment$/)减少编译体积大小完成之后得到如下构建对比结果:增量构建Development 构建Production 构建备注3088ms43702ms89371ms 2561ms27864ms67441ms减少编译体积大小将大型库外链 && 将库预先编译为了避免一些已经编译好的大型库重新编译,我们需要将这些库放在编译意外的地方,或者预先编译这些库。webpack也为我们提供了将模块外链的配置externals,比如我们把lodash外链,配置如下module.exports = { //… externals : { lodash: ‘window.’ }, // 或者 externals : { lodash : { commonjs: ’lodash’, amd: ’lodash’, root: ‘’ // 指向全局变量 } }};针对库预先编译,webpack也提供了相应的插件,那就是webpack.Dllplugin,这个插件可以预先编译制定好的库,最后在实际项目中使用webpack.DllReferencePlugin将预先编译好的库关联到当前的编译结果中,无需重新编译。Dllplugin配置文件webpack.dll.config.js如下:dllReference配置文件webpack.dll.reference.config.js如下:最后使用webpack-merge将webpack.dll.reference.config.js合并到到webpack配置中。注意:预先编译好的库文件需要在html中手动引入并且必须放在webpack的entry引入之前,否则会报错。其实,将大型库外链和将库预先编译也属于减少编译体积的一种,最后得到编译时间结果如下:增量构建Development 构建Production 构建备注3088ms43702ms89371ms 2561ms27864ms67441ms减少编译体积大小2246ms22870ms50601msDll优化后使用缓存首先,我们开启babel-loader自带的缓存功能(默认其实就是打开的)。另外,开启uglifyjs-webpack-plugin的缓存功能。添加缓存插件hard-source-webpack-plugin(当然也可以添加cache-loader)const hardSourcePlugin = require(‘hard-source-webpack-plugin’);moudle.exports = { // … plugins: [ new hardSourcePlugin() ], // …}添加缓存后编译结果如下:增量构建Development 构建Production 构建备注3088ms43702ms89371ms 2561ms27864ms67441ms减少编译体积大小2246ms22870ms50601msDll优化后1918ms10056ms17298ms使用缓存后可以看到,编译效果极好。并行编译由于nodejs为单线程,为了更好利用好电脑多核的特性,我们可以将编译并行开始,这里我们使用happypack,当然也可以使用thread-loader,我们将babel-loader和样式的loader交给happypck接管。babel-loader配置如下:less-loader配置如下:构建结果如下:增量构建Development 构建Production 构建备注3088ms43702ms89371ms 2561ms27864ms67441ms减少编译体积大小2246ms22870ms50601msDll优化后1918ms10056ms17298ms使用缓存后2252ms11846ms18727ms开启happypack后可以看到,添加happypack之后,编译时间有所增加,针对这个结果,我对webpack版本和项目大小进行了对比测试,如下:Webpack:v2.7.0项目:1013个模块全量production构建:105395ms添加happypack之后,全量production构建时间降低到58414ms。针对webpack版本:Webpack:v4.23.0项目:1013个模块全量development构建 : 12352ms添加happypack之后,全量development构建降低到11351ms。得到结论:Webpack v4 之后,happypack已经力不从心,效果并不明显,而且在小型中并不适用。所以针对并行加载方案要不要加,要具体项目具体分析。性能优化对于webpack编译出来的结果,也有相应的性能优化的措施。方案如下:减少模块数量及大小合理缓存合理拆包减少模块数量及大小针对减少模块数量及大小,我们在构建优化的章节中有提到很多,具体点如下:按需加载 babel-plugin-import(antd、iview、bizcharts)、babel-plugin-component(element-ui)减少无用模块webpack.ContextReplacementPlugin、webpack.IgnorePluginTree-shaking:树摇功能,消除无用代码,无用模块。Scope-Hoisting:作用域提升。babel-plugin-transform-runtime,针对babel-polyfill清除不必要的polyfill。前面两点我们就不具体描述,在构建优化章节中有说。Tree-shaking树摇功能,将树上没用的叶子摇下来,寓意将没有必要的代码删除。该功能在webapck V2中已被webpack默认开启,但是使用前提是,模块必须是ES6模块,因为ES6模块为静态分析,动态引入的特性,可以让webpack在构建模块的时候知道,那些模块内容在引入中被使用,那些模块没有被使用,然后将没有被引用的的模块在转为为AST后删除。由于必须使用ES6模块,我们需要将babel的自动模块转化功能关闭,否则你的es6模块将自动转化为commonjs模块,配置如下:{ “presets”: [ “react”, “stage-2”, [ “env”, { “modlues”: false // 关闭babel的自动转化模块功能,保留ES6模块语法 } ] ]}Tree-shaking编译时候可以在命令后使用–display-used-exports可以在shell打印出关于代码剔除的提示。Scope-Hoisting作用域提升,尽可能的把打散的模块合并到一个函数中,前提是不能造成代码冗余。因此只有那些被引用了一次的模块才能被合并。可能不好理解,下面demo对比一下有无Scope-Hoisting的编译结果。首先定义一个util.js文件export default ‘Hello,Webpack’;然后定义入口文件main.jsimport str from ‘./util.js’console.log(str);下面是无Scope-Hoisting结果:然后是Scope-Hoisting后的结果:与Tree-Shaking类似,使用Scope-Hoisting的前提也是必须是ES6模块,除此之外,还需要加入webpack内置插件,位于webpack文件夹,webpack/lib/optimize/ModuleConcatenationPlugin,配置如下:const ModuleConcatenationPlugin = require(‘webpack/lib/optimize/ModuleConcatenationPlugin’);module.exports = { //… plugins: [ new ModuleConcatenationPlugin() ] //…}另外,为了跟好的利用Scope-Hoisting,针对Npm的第三方模块,它们也可能提供了ES6模块,我们可以指定优先使用它们的ES6模块,而不是使用它们编译后的代码,webpack的配置如下:module.exports = { //… resolve: { // 优先采用jsnext:main中指定的ES6模块文件 mainFields: [‘jsnext:main’, ‘module’, ‘browser’, ‘main’] } //…}jsnext:main为业内大家约定好的存放ES6模块的文件夹,后续为了规范,更改为module文件夹。babel-plugin-transform-runtime在我们实际的项目中,为了兼容一些老式的浏览器,我们需要在项目加入babel-polyfill这个包。由于babel-polyfill太大,导致我们编译后的包体积增大,降低我们的加载性能,但是实际上,我们只需要加入我们使用到的不兼容的内容的polyfill就可以,这个时候babel-plugin-transform-runtime就可以帮我们去除那些我们没有使用到的polyfill,当然,你需要在babal-preset-env中配置你需要兼容的浏览器,否则会使用默认兼容浏览器。添加babel-plugin-transform-runtime的.babelrc配置如下:{ “presets”: [[“env”, { “targets”: { “browsers”: [“last 2 versions”, “safari >= 7”, “ie >= 9”, “chrome >= 52”] // 配置兼容浏览器版本 }, “modules”: false }], “stage-2”], “plugins”: [ “transform-class-properties”, “transform-runtime”, // 添加babel-plugin-transform-runtime “transform-decorators-legacy” ]}合理使用缓存webpack对应的缓存方案为添加hash,那我们为什么要给静态资源添加hash呢?避免覆盖旧文件回滚方便,只需要回滚html由于文件名唯一,可开启服务器永远缓然后,webpack对应的hash有两种,hash和chunkhash。hash是跟整个项目的构建相关,只要项目里有文件更改,整个项目构建的hash值都会更改,并且全部文件都共用相同的hash值chunkhash根据不同的入口文件(Entry)进行依赖文件解析、构建对应的chunk,生成对应的哈希值。细想我们期望的最理想的hash就是当我们的编译后的文件,不管是初始化文件,还是chunk文件或者样式文件,只要文件内容一修改,我们的hash就应该更改,然后刷新缓存。可惜,hash和chunkhash的最终效果都没有达到我们的预期。另外,还有来自于的 extract-text-webpack-plugin的 contenthash,contenthash针对编译后的每个文件内容生成hash。只是extract-text-webpack-plugin在wbepack4中已经被弃用,而且这个插件只对css文件生效。webpack-md5-hash为了达到我们的预期效果,我们可以为webpack添加webpack-md5-hash插件,这个插件可以让webpack的chunkhash根据文件内容生成hash,相对稳定,这样就可以达到我们预期的效果了,配置如下:var WebpackMd5Hash = require(‘webpack-md5-hash’); module.exports = { // … output: { //… chunkFilename: “[chunkhash].[id].chunk.js” }, plugins: [ new WebpackMd5Hash() ]};合理拆包为了减少首屏加载的时候,我们需要将包拆分成多个包,然后需要的时候在加载,拆包方案有:第三方包,DllPlugin、externals。动态拆包,利用import()、require.ensure()语法拆包splitChunksPlugin针对第一点第三方包,我们也在第一章节构建优化中有介绍,这里就不详细说了。动态拆包首先是import(),这是webpack提供的语法,webpack在解析到这样的语法时,会将指定的目录文件打包成一个chunk,当成异步加载文件输出到编译结果中,语法如下:import(/* webpackChunkName: chunkName */ ‘./chunkFile.js’).then(_module => { // do something});import()遵循promise规范,可以在then的回调函数中处理模块。注意:import()的参数不能完全是动态的,如果是动态的字符串,需要预先指定前缀文件夹,然后webpack会把整个文件夹编译到结果中,按需加载。然后是require.ensure(),与import()类似,为webpack提供函数,也是用来生成异步加载模块,只是是使用callback的形式处理模块,语法如下:// require.ensure(dependencies: String[], callback: function(require), chunkName: String)require.ensure([], function(require){ const _module = require(‘chunkFile.js’);}, ‘chunkName’);splitChunksPluginwebpack4中,将commonChunksPlugin废弃,引入splitChunksPlugin,两个plugin的作用都是用来切割chunk。webpack 把 chunk 分为两种类型,initial和async。在webpack4的默认情况下,production构建会分析你的 entry、动态加载(import()、require.ensure)模块,找出这些模块之间共用的node_modules下的模块,并将这些模块提取到单独的chunk中,在需要的时候异步加载到页面当中。默认配置如下:module.exports = { //… optimization: { splitChunks: { chunks: ‘async’, // 标记为异步加载的chunk minSize: 30000, minChunks: 1, maxAsyncRequests: 5, maxInitialRequests: 3, automaticNameDelimiter: ‘~’, // 文件名中chunk的分隔符 name: true, cacheGroups: { vendors: { test: /[\/]node_modules[\/]/, priority: -10 }, default: { minChunks: 2, // 最小共享的chunk数 priority: -20, reuseExistingChunk: true } } } }};splitChunksPlugin提供了灵活的配置,开发者可以根据自己的需求分割chunk,比如下面官方的例子1代码:module.exports = { //… optimization: { splitChunks: { cacheGroups: { commons: { name: ‘commons’, chunks: ‘initial’, minChunks: 2 } } } }};意思是在所有的初始化模块中抽取公共部分,生成一个chunk,chunk名字为comons。在如官方例子2代码:module.exports = { //… optimization: { splitChunks: { cacheGroups: { commons: { test: /[\/]node_modules[\/]/, name: ‘vendors’, chunks: ‘all’ } } } }};意思是从所有模块中抽离来自于node_modules下的所有模块,生成一个chunk。当然这只是一个例子,实际生产环境中并不推荐,因为会使我们首屏加载的包增大。针对官方例子2,我们可以在开发环境中使用,因为在开发环境中,我们的node_modules下的所有文件是基本不会变动的,我们将其生产一个chunk之后,每次增量编译,webpack都不会去编译这个来自于node_modules的已经生产好的chunk,这样如果项目很大,来源于node_modules的模块非常多,这个时候可以大大降低我们的构建时间。最后现在大部分前端项目都是基于webpack进行构建的,面对这些项目,或多或少都有一些需要优化的地方,或许做优化不为完成KPI,仅为自己有更好的开发体验,也应该行动起来。 ...

January 19, 2019 · 2 min · jiezi

一个完整版的 webpack 通用配置 demo

January 19, 2019 · 0 min · jiezi

VUE-Router路由懒加载,打包问题(下午更改)

1、路由懒加载配置1.1index路由1.2home组件的路由1.3 添加路由导航守卫1.4项目打包1.4.1提示不能直接访问index.html文件,需要放在服务器中才可以访问。根据搜索解决办法,在 config > index.js 文件//assetsPublicPath: ‘/’, 添加.assetsPublicPath: ‘./’,重新打包后,仍然有提示1.4.2暂时不管,将文件放到虚拟机中继续访问,页面可以正常加载,但只要刷新页面就报404错误。后经过查询,需要将项目放在服务器中运行。不能直接访问静态页面。第二步,虚拟机安装nginx2、nginx的安装2.1配置完成后,启动项目报错 1067报错原因很多,一个个排查,因为IP冲突了。将IIS端口换成8082再启动成功访问locahost可以运行页面2.2将文件扔到nginx文件html文件中文件回退有问题,因为没有进行配置nginx.conf 文件配置配置完成后,再次刷新新页面还有问题2.3解决文件配置问题。因为打包前配置了 config > index.js 文件将配置路径还原,再次打包运行就没问题了。

January 18, 2019 · 1 min · jiezi

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

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

January 17, 2019 · 1 min · jiezi

webpack4系列教程(九):开发环境和生产环境

构建开发环境如果你一直跟随我前面的博文,那么你对webpack的基础知识已经有比较深刻的理解了。之前,我们一直执行着:npm run build来打包编译输出我们的代码,本文我们来看看如何构建一个开发环境,来使我们的开发变得方便些。1.1 webpack-dev-serverwebpack-dev-server是一个简单的小型的web服务器,并且能够实时重载,配置也很简单,首先安装:npm install –save-dev webpack-dev-server配置webpack.config.js:devServer: { port: 8080, // 端口号 host: ‘0.0.0.0’, // 主机名,设为该值可通过IP访问 overlay: { errors: true // 错误提示 } }在package.json中添加命令: “dev”: “cross-env NODE_ENV=development webpack-dev-server –config config/webpack.config.js"执行:npm run dev可见我们的服务已经跑起来了:1.2 source-map在webpack打包源码时,我们会很难找到错误的出现位置,比如将源文件 sum.js、minus.js打包到bundle.js中,其中一个源文件出现了错误,仅仅会追踪到bundle.js中,这对我们来说并不理想。因此为了更加便捷的找到错误的原始位置,JavaScript为我们提供了 source-map的功能,将编译后的代码映射回原始源代码。如果一个错误来自于 sum.js,source map 就会明确的告诉你。 我们来测试一下,在sum.js中输出一个错误:// ES Mudule 规范export default function (a, b) { console.error(’this is test’) // 输出错误 return a + b}在没有devtool配置的情况下 npm run dev,会发现错误提示的行数并不准确,原因是我们的代码是被编译过的然后在webpack.config.js中加入配置: devtool: ‘inline-source-map’, // 加入devtool配置当配置文件改动时需要重新执行 npm run dev:错误提示行数以及源码映射都是正确的。devtool的取值有很多,大家可根据需要自行配置1.3 模块热替换模块热替换(Hot Module Replacement)是 webpack 提供的最有用的功能之一。它允许在运行时更新各种模块,而无需进行完全刷新。使用非常简单,在webpack.config.js中引入webpack: const webpack = require(‘webpack’)在plugins数组中添加: new webpack.NamedModulesPlugin(), new webpack.HotModuleReplacementPlugin()给devServer中的hot属性设为true:devServer: { port: 8080, // 端口号 host: ‘0.0.0.0’, // 主机名,设为该值可通过IP访问 overlay: { errors: true // 错误提示 }, hot: true }这样我们修改代码的时候就可以局部刷新模块而不是刷新整个页面了。2.构建生产环境开发环境(development)和生产环境(production)的构建目标差异很大。在开发环境中,我们需要具有强大的、具有实时重新加载(live reloading)或热模块替换(hot module replacement)能力的 source map 和 localhost server。而在生产环境中,我们的目标则转向于关注更小的 bundle,更轻量的 source map,以及更优化的资源,以改善加载时间。由于要遵循逻辑分离,我们通常建议为每个环境编写彼此独立的 webpack 配置。虽然,以上我们将生产环境和开发环境做了略微区分,但是,请注意,我们还是会遵循不重复原则(Don’t repeat yourself - DRY),保留一个“通用”配置。为了将这些配置合并在一起,我们将使用一个名为 webpack-merge 的工具。通过“通用”配置,我们不必在环境特定的配置中重复代码。我们先从安装 webpack-merge 开始,用来合并webpack配置项:npm install –save-dev webpack-merge在config文件夹下创建 webpack.dev.js 和 webpack.build.js 并修改 webpack.config.js,将开发与生产环境的公共配置放在webpack.config.js中:const path = require(‘path’)const HtmlWebpackPlugin = require(‘html-webpack-plugin’)const MiniCssExtractPlugin = require(‘mini-css-extract-plugin’)const isDev = process.env.NODE_ENV === ‘development’const config = { entry: { main: path.join(__dirname, ‘../src/main.js’) }, output: { filename: ‘[name].bundle.js’, path: path.join(__dirname, ‘../dist’) }, module: { rules: [ { test: /.(vue|js|jsx)$/, loader: ’eslint-loader’, exclude: /node_modules/, enforce: ‘pre’ }, { test: /.js$/, loader: ‘babel-loader’, exclude: /node_modules/ }, { test: /.vue$/, loader: ‘vue-loader’, options: createVueLoaderOptions(isDev) }, { test: /.ejs$/, use: [’ejs-loader’] }, { test: /.css$/, use: [ isDev ? ‘vue-style-loader’ : MiniCssExtractPlugin.loader, { loader: ‘css-loader’, options: { importLoaders: 1 } }, ‘postcss-loader’ ] }, { test: /.less$/, use: [ isDev ? ‘vue-style-loader’ : MiniCssExtractPlugin.loader, ‘css-loader’, { loader: ‘postcss-loader’, options: { sourceMap: true } }, ’less-loader’ ] }, { test: /.(jpg|jpeg|png|gif|svg)$/, use: [ { loader: ‘url-loader’, options: { name: ‘[path][name]-[hash:5].[ext]’, limit: 1024 } } ] } ] }, plugins: [ new HtmlWebpackPlugin({ template: path.join(__dirname, ‘../index.html’), inject: true, minify: { removeComments: true } }) ]}module.exports = configwebpack.dev.jsconst merge = require(‘webpack-merge’)const common = require(’./webpack.config.js’)module.exports = merge(common, { mode: ‘development’, devtool: ‘inline-source-map’, devServer: { port: 8080, host: ‘0.0.0.0’, overlay: { errors: true }, historyApiFallback: { index: ‘/index.html’ } }})webpack.build.jsconst path = require(‘path’)const CleanWebpackPlugin = require(‘clean-webpack-plugin’)const MiniCssExtractPlugin = require(‘mini-css-extract-plugin’)const merge = require(‘webpack-merge’)const common = require(’./webpack.config.js’)module.exports = merge(common, { mode: ‘production’, optimization: { splitChunks: { chunks: ‘initial’, automaticNameDelimiter: ‘.’, cacheGroups: { commons: { name: ‘commons’, chunks: ‘initial’, minChunks: 2, priority: 3 }, vendors: { test: /[\/]node_modules[\/]/, priority: 1 } } }, runtimeChunk: { name: entrypoint => manifest.${entrypoint.name} } }, plugins: [ new MiniCssExtractPlugin({ filename: ‘[name].css’ }), new CleanWebpackPlugin( [‘dist’], { root: path.join(__dirname, ‘../’) } ) ]})修改package.json的命令:“dev”: “cross-env NODE_ENV=development webpack-dev-server –config config/webpack.dev.js”,“build”: “cross-env NODE_ENV=production webpack –config config/webpack.build.js –progress –inline –colors"现在分别执行 npm run dev 和 npm run build 就会得到你想要的了。本人才疏学浅,如有不当之处,欢迎批评指正

January 17, 2019 · 2 min · jiezi

解决.vue文件url引用文件的问题

解决.vue文件url引用文件的问题遇到的问题:在css中引入图片,明明目录结构是对的,还是This dependency was not found;dev好好的,build 之后凉凉,图片加载404添加图片路径配置webpack 添加 alias//webpack.base.conf.js alias: { ‘@’: resolve(‘src’), //加入 ‘assets’: resolve(‘src/assets’) }路径书写规则template 可使用@、style 只能使用script 只能使用@不需要经过打包的static文件写相对路径根据limit:10000,使用两张图片:<template> <div> <div> img+src:@ <img src="@/assets/images/jiaban.jpg" height=“200px”> <img src="@/assets/images/cat.png" alt=""> </div> <div> img+src:~ <img src="~assets/images/jiaban.jpg" height=“200px”> <img src="~assets/images/cat.png" alt=""> </div> <div> img+js(attrs): <img :src=“jiaban” height=“200px”> <img :src=“cat” alt=""> <ul> <li>{{jiaban}}</li> <li>{{cat}}</li> </ul> </div> <div class=“css-bg”> img+css(background-images): <span class=“css-bg__1”></span> <span class=“css-bg__2”></span> </div> <div> static: <img src=“static/images/jiaban.jpg” height=“200px”> <img src=“static/images/cat.png” alt=""> </div> </div></template><script>const jiaban = require(’@/assets/images/jiaban.jpg’);const cat = require(’@/assets/images/cat.png’);export default { data(){ return { jiaban, cat } }}</script><style lang=“scss”>.css-bg__1,.css-bg__2{ display: inline-block;}.css-bg__1{ height: 200px; width: 173px; background-image: url(~assets/images/jiaban.jpg); background-size: contain;}.css-bg__2{ height: 49px; width: 49px; background-image: url(~assets/images/cat.png); background-size: contain;}</style>开发环境截图:添加构建路径配置添加ExtractTextPlugin中publicPath配置,这里根据实际情况配://build/util.js // Extract CSS when that option is specified // (which is the case during production build) if (options.extract) { return ExtractTextPlugin.extract({ use: loaders, fallback: ‘vue-style-loader’, publicPath: ‘../../’, }) } else { return [‘vue-style-loader’].concat(loaders) }生产环境截图:参考链接https://vue-loader.vuejs.org/zh/https://github.com/vuejs/vue-loader/issues/193https://segmentfault.com/q/1010000004582219 ...

January 17, 2019 · 1 min · jiezi

spa 项目上线后程序刷新一次浏览器,来使用新上线资源

解决了什么问题?spa项目新上线后,登陆有效期内用户,可以马上使用新上线资源。原理:路由切换时,判断如果是新上线,程序刷新下浏览器。实现步骤:打包时产生一个json文件:static/json/build_str.jsonlocalStorage中存入值:build_str每个路由切换时,从接口获得新打包后json中的字符串,与localStorage中存的上次打包字符串比较,不相同时刷新vue 项目代码修改的地方:1、相应目录下,新建文件:static/json/build_str.json2、build/build.js 修改:// 将当前时间戳写入json文件let json_obj = {“build_str”: new Date().getTime().toString()}fs.writeFile(path.resolve(__dirname, ‘../static/json/build_str.json’), JSON.stringify(json_obj), function (err) { if (err) { return console.error(err); } console.log(“打包字符串写入文件:static/json/build_str.json,成功!”); realBuild()})3、src/main.js 修改:router.beforeEach((to, from, next) => { axios.get(’/static/json/build_str.json?v=’ + new Date().getTime().toString()) .then(res => { let newBuildStr = res.data.build_str let oldBuildStr = localStorage.getItem(‘build_str’) || ’’ if (oldBuildStr !== newBuildStr) { console.log(‘auto refresh’) localStorage.setItem(‘build_str’, newBuildStr) location.reload() } }) next()})项目demo:https://github.com/cag2050/vu…

January 16, 2019 · 1 min · jiezi

uni-app跨域解决方案

1.官方推荐cors和插件安装解决跨域2.配置uni-app 中 manifest.json->h5->devServermanifest.json “h5”: { “devServer”: { “port”: 8000, “disableHostCheck”: true, “proxy”: { “/dpc”: { “target”: “http://dpc.dapeis.net”, “changeOrigin”: true, “secure”: false } } } }http请求uni.request({ url: ‘/dpc/getUserInfo’, success: (res) => { console.log(res.data); }});这样请求webpack会解析为请求http://dpc.dapeis.net/dpc/参考资料:webpack-dev-serverwebpack跨域API

January 16, 2019 · 1 min · jiezi

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

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

January 15, 2019 · 1 min · jiezi

vue-cli新建的项目webpack设置涉及的大部分插件整理

portfinder用来检测未占用的端口更多看这里: https://www.npmjs.com/package/portfinderwebpack-merge用来合并多个webpack设置,也可以合并对象更多看这里: https://www.npmjs.com/package/friendly-errors-webpack-pluginhtml-webpack-plugin将html复制并插入一些打包后的依赖路径的插件,开发vue中必用的webpack插件之一!更多看这里: https://www.npmjs.com/package/html-webpack-plugincopy-webpack-plugin将static中文件原样复制到dist的插件,开发vue必用的webpack插件之一!更多看这里:https://www.npmjs.com/package/copy-webpack-pluginextract-text-webpack-plugin将包或包中的文本提取到单独的文件中的webpack插件,开发vue必用的webpack插件之一!更多看这里: https://www.npmjs.com/package/extract-text-webpack-pluginoptimize-css-assets-webpack-plugin优化最小化CSS的一个webpack插件,开发vue必用的webpack插件之一!更多看这里:https://www.npmjs.com/package/optimize-css-assets-webpack-pluginuglifyjs-webpack-plugin压缩/混淆 JavaScript代码的webpack插件,开发vue必用的webpack插件之一!更多看这里:https://www.npmjs.com/package/uglifyjs-webpack-pluginora一个友好的命令行界面提示插件,有可以转圈的图标更多看这里:https://www.npmjs.com/package/orarimraf类似于 Unix系统的 rm -rf 删除命令的插件更多看这里:https://www.npmjs.com/package/rimrafchalk可以在命令行打印出彩色的字体更多看这里:https://www.npmjs.com/package/chalkshelljs可以运行Unix命令行的Nodejs插件更多看这里:https://www.npmjs.com/package/shelljssemver语义化版本控制插件更多看这里:https://www.npmjs.com/package/semver整理还是很辛苦的,大家且看且珍惜!首发简书:https://www.jianshu.com/p/e15…

January 15, 2019 · 1 min · jiezi

关于css打包后过大的问题

公共代码多次放在scoped内我们很多时候写了一个公共的common.scss,然后在各个vue里面<style scoped>import ‘@/assets/css/common.scss’</style>由于加了scoped,导致common里面的样式都加上了[data-v-aaaa]这样的标识,最后打出来的css重复,这就不是公共css了。并且在多少个vue里面import就会打包多少次common.scss进去,最后导致css打出来很大。公共代码多次放在scoped外下面的代码打包出来只会有一份global.scss,也就是不会重复打包。所以为了方便,直接在main.js引入一次就够了//a.vue<style >@import ‘@/assets/css/global.scss’</style>//b.vue<style >@import ‘@/assets/css/global.scss’</style>公共代码放scoped外,自定义代码放scoped内为了不重复打包,那我把公共的css代码拿出来不放在scoped里面不就好了。//global.scss.white{ color:#fff}//a.vue<style >@import ‘@/assets/css/global.scss’</style>但是下面这个情况不行,运行会报错:$white is not defined// variable.scss$white:#fff;//a.vue<style >@import ‘@/assets/css/variable.scss’</style><style scoped>.white{ color:$white;}<style/>然后试了一下<style scoped>@import ‘@/assets/css/variable.scss’.white{ color:$white;}</style>发现打出来的css里面变量$white直接编译成#fff了,打包后的css里面找不到$white,所以就不用担心会重复打包这些变量了。IE浏览器对css的约束之前控制台中心在IE9浏览器下样式混乱。说是IE浏览器对css有些约束Stylesheet Limits in Internet Explorer参考博客然后用了下面两个插件解决。好像也可以设置splitChunk解决,不过试了效果不太满意,后面再继续探究。optimization: { minimizer: [ new CSSSplitWebpackPlugin({ size: 4000, filename: path.posix.join(assetsDir,‘css/[name]-[part].[ext]’), }), new OptimizeCSSAssetsPlugin({ assetNameRegExp: /.css$/g, cssProcessor: require(‘cssnano’), cssProcessorOptions: { discardComments: {removeAll: true },reduceIdents:false }, canPrint: true }), ]},总结:把css变量单独拎出来做一个文件variable.scss,在vue的<style scoped>里@import使用公共css文件在main.js里面直接import一次就好,不要再在其他vue里面import多人协作防止冲突,各组件的公共css文件我觉得可以在外面用一个自己的class包裹起来。例如//main.jsimport ‘@/views/order/style.scss’;//@/views/order/style.scss.order{ xxxxx}最后把代码按照上面的方法整理了一下,结果打包出来的css足足小了127kb

January 15, 2019 · 1 min · jiezi

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

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

January 15, 2019 · 1 min · jiezi

webpack打包dist文件导出配置文件.json供外部配置

大家在日常开发上线,上预发,测试环境的时候,一定遇到过不同环境,配置项不同的情况,比如不同的环境部署的代码一致,只有Host需要配置不同。这个时候,如果每次都要重新修改代码,不仅显得不专业,也容易在提交的时候忘记这个配置,提交错误就是上线灾难。所以,有必要在打包后的文件中生成配置文件,供在外部修改配置。本篇就是为了解决这个问题而写~~这要用到一个webpack插件generate-asset-webpack-pluginInstallnpm install –save-dev generate-asset-webpack-plugin在webpack.prod.cong.js中引入该插件const GeneraterAssetPlugin = require(‘generate-asset-webpack-plugin’)在项目根目录下建立配置文件serverConfig.json内部配置为{“baseUrl”:""}将该配置文件引入const serverConfig = require(’../serverConfig.json’)将Json中的配置项返回const createJson = function(compilation) { return JSON.stringify(serverConfig)}在webpackConfig -> plugin中写入new GeneraterAssetPlugin({ filename: ‘serverConfig.json’,//输出到dist根目录下的serverConfig.json文件,名字可以按需改 fn: (compilation, cb) => { cb(null, createJson(compilation)); }})在引入axios的文件中做如下配置axios.get(‘serverConfig.json’).then((result) => { window.localStorage[‘JSON_HOST’] = result.data.baseUrl}).catch((error) => { console.log(error) });利用localStorage存储JSON_HOST配置值,需要取时再取出JSON_HOST,再将需要配置host的地方写成host: window.localStorage[‘JSON_HOST’]打包后在根目录就会生成如下dist文件外部就可以通过修改该配置文件,变更host了

January 15, 2019 · 1 min · jiezi

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

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

January 15, 2019 · 1 min · jiezi

Webpack 的 Bundle Split 和 Code Split 区别和应用

Webpack Bundle Split 和 Code Split话说之前也是对 chunk 这个概念有些模糊,并且很多时候网上的文章大部分在将代码分离动态加载之类的。写这篇文章的目的也是想让其他那些跟我一样曾经对这个概念不是很清楚的童鞋有个清晰的认识。废话不多说,撸起袖子直接干!Let’s Dive in!Webpack 文件分离包括两个部分,一个是 Bundle 的分离,一个是 Code 代码的分离:Bundle splitting: 实际上就是创建多个更小的文件,并行加载,以获得更好的缓存效果;主要的作用就是使浏览器并行下载,提高下载速度。并且运用浏览器缓存,只有代码被修改,文件名中的哈希值改变了才会去再次加载。Code splitting: 只加载用户最需要的部分,其余的代码都遵从懒加载的策略;主要的作用就是加快页面加载速度,不加载不必要加载的东西。准备工作在进行文件分离之前的准备工作,我们先写一些代码:入口文件 src/index.js:const { getData } = require(’./main’)const { findMaxIndex } = require(’./math’)let arr = [1,2,123,21,3,21,321,1]findMaxIndex(arr)getData(’./index.html’)两个依赖模块:src/main.js:const axios = require(‘axios’)const getData = url => { axios.get(url).then(d => { console.log(d.status) console.log(d.data.length) })}module.exports = { getData}src/math.js:const _ = require(’lodash’)const findMaxIndex = arr => { let x = _.max(arr) let r = Array.prototype.indexOf.call(arr, x) console.log(r);}module.exports = { findMaxIndex}增加一个 webpack 配置文件 webpack.config.js:const path = require(‘path’)module.exports = { mode: ‘development’, entry: path.resolve(__dirname, ‘src/index.js’), output: { path: path.resolve(__dirname, ‘dist’), filename: ‘[name].[contenthash].js’ },}文件分离之前打包需要的时间在 bundle split 和 code split 操作之前,我们先看一下当前默认打包的效果:全部依赖都被打包到 main.xxx.js 中去,大小是 609k开始分离操作Bundle SplitBundle Split 的主要任务是将多个引用的包和模块进行分离,避免全部依赖打包到一个文件下基本用法Webpack 4 中需要使用到 optimization.splitChunks 的配置:const path = require(‘path’)module.exports = { mode: ‘development’, entry: path.resolve(__dirname, ‘src/index.js’), output: { path: path.resolve(__dirname, ‘dist’), filename: ‘[name].[contenthash].js’ }, optimization: { splitChunks: { chunks: ‘all’ } }}optimization.splitChunks 的意思是将所有来自 node_modules 中的依赖全部打包分离出来,这个时候我们再看打包的文件是个什么样子:增加了 splitChunks 的配置,我们第三方模块都被打包到了 vendors~main.xxx.js 中去了,这个文件大小有 604k,而入口文件 main.xxx.js 则只有 7k虽然说这样将第三方模块单独打包出去能够减小入口文件的大小,但这样仍然是个不小的文件;这个小的测试项目中我们使用到了 axios 和 lodash 这两个第三方模块,因此我们希望的应该是将这两个模块单独分离出来两个文件,而不是全部放到一个 vendors 中去,那么我们继续配置 webpack.config.js:将每个 npm 包单独分离出来这里我们需要使用到 webpack.HashedModuleIdsPlugin 这个插件参考官方文档直接上代码:const path = require(‘path’)const webpack = require(‘webpack’)module.exports = { mode: ‘development’, entry: path.resolve(__dirname, ‘src/index.js’), plugins: [ new webpack.HashedModuleIdsPlugin() // 根据模块的相对路径生成 HASH 作为模块 ID ], output: { path: path.resolve(__dirname, ‘dist’), filename: ‘[name].[contenthash].js’ }, optimization: { runtimeChunk: ‘single’, splitChunks: { chunks: ‘all’, // 默认 async 可选值 all 和 initial maxInitialRequests: Infinity, // 一个入口最大的并行请求数 minSize: 0, // 避免模块体积过小而被忽略 minChunks: 1, // 默认也是一表示最小引用次数 cacheGroups: { vendor: { test: /[\/]node_modules[\/]/, // 如果需要的依赖特别小,可以直接设置成需要打包的依赖名称 name(module, chunks, chcheGroupKey) { // 可提供布尔值、字符串和函数,如果是函数,可编写自定义返回值 const packageName = module.context.match(/[\/]node_modules\//)[1] // 获取模块名称 return npm.${packageName.replace('@', '')} // 可选,一般情况下不需要将模块名称 @ 符号去除 } } } } }}这里我们主要做了几件事:为了避免每次打包的文件哈希变化,我们可以使用 webpack 内置的 HashedModuleIdsPlugin,这样可以避免每次打包的文件哈希值变化首先增加 maxInitialRequests 并设置成 Infinity,指定这个入口文件最大并行请求数然后将 minSize 和 minChunks 分别设置成 0 和 1,即使模块非常小也将其提取出来,并且这个模块的引用次数只有 1 也要提取最后配置匹配的依赖以及分离出的文件名格式另外,我们还将运行时代码分离出来,这块代码还可以配合 InlineManifestWebpackPlugin 直接插入到 HTML 文件中。这里我们将这个配置设置成 single,即将所有chunk的运行代码打包到一个文件中这样 Bundle Split 的操作基本就完成了,让我们看看效果如何:所依赖的几个模块都被分离出去了使用 HtmlWebpackPlugin 这个插件将 js 代码注入 html 文件中npm i -D html-webpack-plugin修改 webpack.config.js 文件:// 配置文件引入这个插件var HtmlWebpackPlugin = require(‘html-webpack-plugin’);// …module.exports = { // … plugins: [ new HtmlWebpackPlugin(), new webpack.HashedModuleIdsPlugin() // 根据模块的相对路径生成 HASH 作为模块 ID ], // …}安装 http-server 或使用 vscode 的插件 Live Server 将代码放入一个本地服务器中,打开浏览器的调试窗口进入到 Network 面板:可以看到我们将模块单独分离出来并行加载,这样比单独加载一个庞大的包要快不少,接下来我们还要进行代码分离,将不必要加载的模块延迟加载Code Split代码分离实际上就是只加载用户需要使用到的部分代码,不是必须的就暂时不加载。这里我们要用到 require.ensure 这个方法去获取依赖,这样 webpack 打包之后将会增加运行时代码,在设定好的条件下才会触发获取这个依赖。在 ES6 中我们可以用 Dynamic Imports 来替代上述方案,如果使用 ES6 语法那么需要使用到 babel 以及 babel 的插件 plugin-syntax-dynamic-import,在浏览器中为了保证兼容性,还需要安装 promise 的 polyfill,用法大同小异,可直接观摩 webpack 的官方文档修改我们的代码:const { getData } = require(’./main’)let arr = [1,2,123,21,3,21,321,1]getData(’./index.html’)setTimeout(() => { require.ensure([’./math’], function(require) { const { findMaxIndex } = require(’./math’) console.log(findMaxIndex(arr)) })}, 3000)我们设定了一个定时器,只有在 3000 毫秒以后,浏览器才会去请求这个 math 模块编译之后,打开调试面板刷新浏览器:在页面刚加载完毕后,浏览器会尝试获取上述这么几个模块,因为模块都很小很快就加载完成了在 3500ms 左右,浏览器才会去获取 requie.ensure 方法定义的 math 模块,因 math 模块又包含依赖 lodash,因此这个 lodash 第三方模块也会被按需加载。这样我们就完成了代码分离的操作,这样做的优势就是不需要第一时间加载的模块,可以推迟加载,以页面的加载速度。当然,上面的 timeout 定时器完全可以换成其他诸如按钮点击之类的事件来触发。END ...

January 15, 2019 · 2 min · jiezi

超详细的webpack原理解读

webpack原理解读本文抄自《深入浅出webpack》,建议想学习原理的手打一遍,操作一遍,给别人讲一遍,然后就会了在阅读前希望您已有webpack相关的实践经验,不然读了也读不懂本文阅读需要几分钟,理解需要自己动手操作蛮长时间0 配置文件首先简单看一下webpack配置文件(webpack.config.js):var path = require(‘path’);var node_modules = path.resolve(__dirname, ’node_modules’);var pathToReact = path.resolve(node_modules, ‘react/dist/react.min.js’);module.exports = { // 入口文件,是模块构建的起点,同时每一个入口文件对应最后生成的一个 chunk。 entry: { bundle: [ ‘webpack/hot/dev-server’, ‘webpack-dev-server/client?http://localhost:8080’, path.resolve(__dirname, ‘app/app.js’) ] }, // 文件路径指向(可加快打包过程)。 resolve: { alias: { ‘react’: pathToReact } }, // 生成文件,是模块构建的终点,包括输出文件与输出路径。 output: { path: path.resolve(__dirname, ‘build’), filename: ‘[name].js’ }, // 这里配置了处理各模块的 loader ,包括 css 预处理 loader ,es6 编译 loader,图片处理 loader。 module: { loaders: [ { test: /.js$/, loader: ‘babel’, query: { presets: [’es2015’, ‘react’] } } ], noParse: [pathToReact] }, // webpack 各插件对象,在 webpack 的事件流中执行对应的方法。 plugins: [ new webpack.HotModuleReplacementPlugin() ]};1. 工作原理概述1.1 基本概念在了解webpack原理之前,需要掌握以下几个核心概念Entry: 入口,webpack构建第一步从entry开始module:模块,在webpack中一个模块对应一个文件。webpack会从entry开始,递归找出所有依赖的模块Chunk:代码块,一个chunk由多个模块组合而成,用于代码合并与分割Loader: 模块转换器,用于将模块的原内容按照需求转换成新内容Plugin:拓展插件,在webpack构建流程中的特定时机会广播对应的事件,插件可以监听这些事件的发生,在特定的时机做对应的事情1.2 流程概述webpack从启动到结束依次执行以下操作:graph TD初始化参数 –> 开始编译 开始编译 –>确定入口 确定入口 –> 编译模块编译模块 –> 完成编译模块完成编译模块 –> 输出资源输出资源 –> 输出完成各个阶段执行的操作如下:初始化参数:从配置文件(默认webpack.config.js)和shell语句中读取与合并参数,得出最终的参数开始编译(compile):用上一步得到的参数初始化Comiler对象,加载所有配置的插件,通过执行对象的run方法开始执行编译确定入口:根据配置中的entry找出所有的入口文件编译模块:从入口文件出发,调用所有配置的Loader对模块进行翻译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过处理完成编译模块:经过第四步之后,得到了每个模块被翻译之后的最终内容以及他们之间的依赖关系输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的chunk,再将每个chunk转换成一个单独的文件加入输出列表中,这是可以修改输出内容的最后机会输出完成:在确定好输出内容后,根据配置(webpack.config.js && shell)确定输出的路径和文件名,将文件的内容写入文件系统中(fs)在以上过程中,webpack会在特定的时间点广播特定的事件,插件监听事件并执行相应的逻辑,并且插件可以调用webpack提供的api改变webpack的运行结果1.3 流程细节webpack构建流程可分为以下三大阶段。初始化:启动构建,读取与合并配置参数,加载plugin,实例化Compiler编译:从Entry出发,针对每个Module串行调用对应的Loader去翻译文件中的内容,再找到该Module依赖的Module,递归的进行编译处理输出:将编译后的Module组合成Chunk,将Chunk转换成文件,输出到文件系统中如果只执行一次,流程如上,但在开启监听模式下,流程如下图graph TD 初始化–>编译; 编译–>输出; 输出–>文本发生变化 文本发生变化–>编译1.3.1初始化阶段在初始化阶段会发生的事件如下事件描述初始化参数从配置文件和shell语句中读取与合并参数,得出最终的参数,这个过程还会执行配置文件中的插件实例化语句 new Plugin()实例化Compiler实例化Compiler,传入上一步得到的参数,Compiler负责文件监听和启动编译。在Compiler实例中包含了完整的webpack配置,全局只有一个Compiler实例。加载插件依次调用插件的apply方法,让插件可以监听后续的所有事件节点。同时向插件中传入compiler实例的引用,以方便插件通过compiler调用webpack的apienvironment开始应用Node.js风格的文件系统到compiler对象,以方便后续的文件寻找和读取Entry-option读取配置的Entrys,为每个Entry实例化一个对应的EntryPlugin,为后面该Entry的递归解析工作做准备After-plugins调用完所有内置的和配置的插件的apply方法After-resolvers根据配置初始化resolver,resolver负责在文件系统中寻找指定路径的文件#### 1.3.2 编译阶段 (事件名全为小写)事件解释run启动一次编译Watch-run在监听模式下启动编译,文件发生变化会重新编译compile告诉插件一次新的编译将要启动,同时会给插件带上compiler对象compilation当webpack以开发模式运行时,每当检测到文件的变化,便有一次新的compilation被创建。一个Compilation对象包含了当前的模块资源、编译生成资源、变化的文件等。compilation对象也提供了很多事件回调给插件进行拓展make一个新的compilation对象创建完毕,即将从entry开始读取文件,根据文件类型和编译的loader对文件进行==编译==,编译完后再找出该文件依赖的文件,递归地编译和解析after-compile一次compilation执行完成invalid当遇到错误会触发改事件,该事件不会导致webpack退出 在编译阶段最重要的事件是compilation,因为在compilation阶段调用了Loader,完成了每个模块的==转换==操作。在compilation阶段又会发生很多小事件,如下表事件解释build-module使用相应的Loader去转换一个模块Normal-module-loader在使用loader转换完一个模块后,使用acorn解析转换后的内容,输出对应的抽象语法树(AST),以方便webpack对代码进行分析program从配置的入口模块开始,分析其AST,当遇到require等导入其他模块的语句时,便将其加入依赖的模块列表中,同时对于新找出来的模块递归分析,最终弄清楚所有模块的依赖关系seal所有模块及依赖的模块都通过Loader转换完成,根据依赖关系生成Chunk 2.3 输出阶段输出阶段会发生的事件及解释:事件解释should-emit所有需要输出的文件已经生成,询问插件有哪些文件需要输出,有哪些不需要输出emit确定好要输出哪些文件后,执行文件输出,==可以在这里获取和修改输出的内容==after-mit文件输出完毕done成功完成一次完整的编译和输出流程failed如果在编译和输出中出现错误,导致webpack退出,就会直接跳转到本步骤,插件可以在本事件中获取具体的错误原因在输出阶段已经得到了各个模块经过转化后的结果和其依赖关系,并且将相应的模块组合在一起形成一个个chunk.在输出阶段根据chunk的类型,使用对应的模板生成最终要输出的文件内容. |//以下代码用来包含webpack运行过程中的每个阶段//file:webpack.config.jsconst path = require(‘path’);//插件监听事件并执行相应的逻辑class TestPlugin { constructor() { console.log(’@plugin constructor’); } apply(compiler) { console.log(’@plugin apply’); compiler.plugin(’environment’, (options) => { console.log(’@environment’); }); compiler.plugin(‘after-environment’, (options) => { console.log(’@after-environment’); }); compiler.plugin(’entry-option’, (options) => { console.log(’@entry-option’); }); compiler.plugin(‘after-plugins’, (options) => { console.log(’@after-plugins’); }); compiler.plugin(‘after-resolvers’, (options) => { console.log(’@after-resolvers’); }); compiler.plugin(‘before-run’, (options, callback) => { console.log(’@before-run’); callback(); }); compiler.plugin(‘run’, (options, callback) => { console.log(’@run’); callback(); }); compiler.plugin(‘watch-run’, (options, callback) => { console.log(’@watch-run’); callback(); }); compiler.plugin(’normal-module-factory’, (options) => { console.log(’@normal-module-factory’); }); compiler.plugin(‘context-module-factory’, (options) => { console.log(’@context-module-factory’); }); compiler.plugin(‘before-compile’, (options, callback) => { console.log(’@before-compile’); callback(); }); compiler.plugin(‘compile’, (options) => { console.log(’@compile’); }); compiler.plugin(’this-compilation’, (options) => { console.log(’@this-compilation’); }); compiler.plugin(‘compilation’, (options) => { console.log(’@compilation’); }); compiler.plugin(‘make’, (options, callback) => { console.log(’@make’); callback(); }); compiler.plugin(‘compilation’, (compilation) => { compilation.plugin(‘build-module’, (options) => { console.log(’@build-module’); }); compilation.plugin(’normal-module-loader’, (options) => { console.log(’@normal-module-loader’); }); compilation.plugin(‘program’, (options, callback) => { console.log(’@program’); callback(); }); compilation.plugin(‘seal’, (options) => { console.log(’@seal’); }); }); compiler.plugin(‘after-compile’, (options, callback) => { console.log(’@after-compile’); callback(); }); compiler.plugin(‘should-emit’, (options) => { console.log(’@should-emit’); }); compiler.plugin(’emit’, (options, callback) => { console.log(’@emit’); callback(); }); compiler.plugin(‘after-emit’, (options, callback) => { console.log(’@after-emit’); callback(); }); compiler.plugin(‘done’, (options) => { console.log(’@done’); }); compiler.plugin(‘failed’, (options, callback) => { console.log(’@failed’); callback(); }); compiler.plugin(‘invalid’, (options) => { console.log(’@invalid’); }); }}#在目录下执行webpack#输出以下内容@plugin constructor@plugin apply@environment@after-environment@entry-option@after-plugins@after-resolvers@before-run@run@normal-module-factory@context-module-factory@before-compile@compile@this-compilation@compilation@make@build-module@normal-module-loader@build-module@normal-module-loader@seal@after-compile@should-emit@emit@after-emit@doneHash: 19ef3b418517e78b5286Version: webpack 3.11.0Time: 95ms Asset Size Chunks Chunk Namesbundle.js 3.03 kB 0 [emitted] main [0] ./main.js 44 bytes {0} [built] [1] ./show.js 114 bytes {0} [built]2 输出文件分析2.1 举个栗子下面通过 Webpack 构建一个采用 CommonJS 模块化编写的项目,该项目有个网页会通过 JavaScript 在网页中显示 Hello,Webpack。运行构建前,先把要完成该功能的最基础的 JavaScript 文件和 HTML 建立好,需要如下文件:页面入口文件 index.html<html><head> <meta charset=“UTF-8”></head><body><div id=“app”></div><!–导入 Webpack 输出的 JavaScript 文件–><script src="./dist/bundle.js"></script></body></html>JS 工具函数文件 show.js// 操作 DOM 元素,把 content 显示到网页上function show(content) { window.document.getElementById(‘app’).innerText = ‘Hello,’ + content;}// 通过 CommonJS 规范导出 show 函数module.exports = show;JS 执行入口文件 main.js// 通过 CommonJS 规范导入 show 函数const show = require(’./show.js’);// 执行 show 函数show(‘Webpack’);Webpack 在执行构建时默认会从项目根目录下的 webpack.config.js 文件读取配置,所以你还需要新建它,其内容如下:const path = require(‘path’);module.exports = { // JavaScript 执行入口文件 entry: ‘./main.js’, output: { // 把所有依赖的模块合并输出到一个 bundle.js 文件 filename: ‘bundle.js’, // 输出文件都放到 dist 目录下 path: path.resolve(__dirname, ‘./dist’), }};由于 Webpack 构建运行在 Node.js 环境下,所以该文件最后需要通过 CommonJS 规范导出一个描述如何构建的 Object 对象。|– index.html|– main.js|– show.js|– webpack.config.js一切文件就绪,在项目根目录下执行 webpack 命令运行 Webpack 构建,你会发现目录下多出一个 dist目录,里面有个 bundle.js 文件, bundle.js 文件是一个可执行的 JavaScript 文件,它包含页面所依赖的两个模块 main.js 和 show.js 及内置的 webpackBootstrap 启动函数。 这时你用浏览器打开 index.html 网页将会看到 Hello,Webpack。2.2 bundle.js文件做了什么看之前记住:一个模块就是一个文件,首先看下bundle.js长什么样子:注意:序号1处是个自执行函数,序号2作为自执行函数的参数传入具体代码如下:(建议把以下代码放入编辑器中查看,最好让index.html执行下,弄清楚执行的顺序)(function(modules) { // webpackBootstrap // 1. 缓存模块 var installedModules = {}; // 2. 定义可以在浏览器使用的require函数 function webpack_require(moduleId) { // 2.1检查模块是否在缓存里,在的话直接返回 if(installedModules[moduleId]) { return installedModules[moduleId].exports; } // 2.2 模块不在缓存里,新建一个对象module=installModules[moduleId] {i:moduleId,l:模块是否加载,exports:模块返回值} var module = installedModules[moduleId] = { i: moduleId,//第一次执行为0 l: false, exports: {} };//第一次执行module:{i:0,l:false,exports:{}} // 2.3 执行传入的参数中对应id的模块 第一次执行数组中传入的第一个参数 //modules[0].call({},{i:0,l:false,exports:{}},{},__webpack_require__函数) modules[moduleId].call(module.exports, module, module.exports, webpack_require); // 2.4 将这个模块标记为已加载 module.l = true; // 2.5 返回这个模块的导出值 return module.exports; } // 3. webpack暴露属性 m c d n o p webpack_require.m = modules; webpack_require.c = installedModules; webpack_require.d = function(exports, name, getter) { if(!webpack_require.o(exports, name)) { Object.defineProperty(exports, name, { configurable: false, enumerable: true, get: getter }); } }; 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; }; webpack_require.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; webpack_require.p = “”; // 4. 执行reruire函数引入第一个模块(main.js对应的模块) return webpack_require(webpack_require.s = 0);})([ // 0. 传入参数,参数是个数组 /* 第0个参数 main.js对应的文件*/ (function(module, exports, webpack_require) { // 通过 CommonJS 规范导入 show 函数 const show = webpack_require(1);//webpack_require(1)返回show // 执行 show 函数 show(‘Webpack’); }), /* 第1个参数 show.js对应的文件 / (function(module, exports) { // 操作 DOM 元素,把 content 显示到网页上 function show(content) { window.document.getElementById(‘app’).innerText = ‘Hello,’ + content; } // 通过 CommonJS 规范导出 show 函数 module.exports = show; })]);以上看上去复杂的代码其实是一个自执行函数(文件作为自执行函数的参数),可以简写如下:(function(modules){ //模拟require语句 function webpack_require(){} //执行存放所有模块数组中的第0个模块(main.js) _webpack_require[0]})([/存放所有模块的数组/])bundles.js能直接在浏览器中运行的原因是,在输出的文件中通过__webpack_require__函数,定义了一个可以在浏览器中执行的加载函数(加载文件使用ajax实现),来模拟Node.js中的require语句。原来一个个独立的模块文件被合并到了一个单独的 bundle.js 的原因在于浏览器不能像 Node.js 那样快速地去本地加载一个个模块文件,而必须通过网络请求去加载还未得到的文件。 如果模块数量很多,加载时间会很长,因此把所有模块都存放在了数组中,执行一次网络加载。修改main.js,改成import引入模块import show from ‘./show’;show(‘Webpack’);在目录下执行webpack,会发现:生成的代码会有所不同,但是主要的区别是自执行函数的参数不同,也就是2.2代码的第二部分不同([//自执行函数和上面相同,参数不同/ 0 /(function(module, webpack_exports, webpack_require) {“use strict”;Object.defineProperty(webpack_exports, “__esModule”, { value: true });/ harmony import / var WEBPACK_IMPORTED_MODULE_0__show = webpack_require(1);Object(WEBPACK_IMPORTED_MODULE_0__show[“a” / default /])(‘Webpack’);}),/ 1 /(function(module, webpack_exports, webpack_require) {“use strict”;/ harmony export (immutable) / webpack_exports[“a”] = show;function show(content) { window.document.getElementById(‘app’).innerText = ‘Hello,’ + content;}})]);参数不同的原因是es6的import和export模块被webpack编译处理过了,其实作用是一样的,接下来看一下在main.js中异步加载模块时,bundle.js是怎样的2.3异步加载时,bundle.js代码分析main.js修改如下import(’./show’).then(show=>{ show(‘Webpack’)})构建成功后会生成两个文件bundle.js 执行入口文件0.bundle.js 异步加载文件其中0.bundle.js文件的内容如下:webpackJsonp(/在其他文件中存放的模块的ID/[0],[//本文件所包含的模块/ 0 /,/ 1 show.js对应的模块 /(function(module, webpack_exports, webpack_require) { “use strict”; Object.defineProperty(webpack_exports, “__esModule”, { value: true }); / harmony export (immutable) */ webpack_exports[“default”] = show; function show(content) { window.document.getElementById(‘app’).innerText = ‘Hello,’ + content; }})]);bundle.js文件的内容如下:注意:bundle.js比上面的bundle.js的区别在于:多了一个__webpack_require.e,用于加载被分割出去的需要异步加载的chunk对应的文件多了一个webpackJsonp函数,用于从异步加载的文件中安装模块(function(modules) { // webpackBootstrap // install a JSONP callback for chunk loading var parentJsonpFunction = window[“webpackJsonp”]; // webpackJsonp用于从异步加载的文件中安装模块 // 将webpackJsonp挂载到全局是为了方便在其他文件中调用 /** * @param chunkIds 异步加载的模块中需要安装的模块对应的id * @param moreModules 异步加载的模块中需要安装模块列表 * @param executeModules 异步加载的模块安装成功后需要执行的模块对应的index / window[“webpackJsonp”] = function webpackJsonpCallback(chunkIds, moreModules, executeModules) { // add “moreModules” to the modules object, // then flag all “chunkIds” as loaded and fire callback var moduleId, chunkId, i = 0, resolves = [], result; for(;i < chunkIds.length; i++) { chunkId = chunkIds[i]; if(installedChunks[chunkId]) { resolves.push(installedChunks[chunkId][0]); } installedChunks[chunkId] = 0; } for(moduleId in moreModules) { if(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) { modules[moduleId] = moreModules[moduleId]; } } if(parentJsonpFunction) parentJsonpFunction(chunkIds, moreModules, executeModules); while(resolves.length) { resolves.shift()(); } }; // The module cache var installedModules = {}; // objects to store loaded and loading chunks var installedChunks = { 1: 0 }; // 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; } // This file contains only the entry chunk. // The chunk loading function for additional chunks /* * 用于加载被分割出去的需要异步加载的chunk对应的文件 * @param chunkId 需要异步加载的chunk对应的id * @returns {Promise} / webpack_require.e = function requireEnsure(chunkId) { var installedChunkData = installedChunks[chunkId]; if(installedChunkData === 0) { return new Promise(function(resolve) { resolve(); }); } // a Promise means “currently loading”. if(installedChunkData) { return installedChunkData[2]; } // setup Promise in chunk cache var promise = new Promise(function(resolve, reject) { installedChunkData = installedChunks[chunkId] = [resolve, reject]; }); installedChunkData[2] = promise; // start chunk loading var head = document.getElementsByTagName(‘head’)[0]; var script = document.createElement(‘script’); script.type = “text/javascript”; script.charset = ‘utf-8’; script.async = true; script.timeout = 120000; if (webpack_require.nc) { script.setAttribute(“nonce”, webpack_require.nc); } script.src = webpack_require.p + "" + chunkId + “.bundle.js”; var timeout = setTimeout(onScriptComplete, 120000); script.onerror = script.onload = onScriptComplete; function onScriptComplete() { // avoid mem leaks in IE. script.onerror = script.onload = null; clearTimeout(timeout); var chunk = installedChunks[chunkId]; if(chunk !== 0) { if(chunk) { chunk[1](new Error(‘Loading chunk ’ + chunkId + ’ failed.’)); } installedChunks[chunkId] = undefined; } }; head.appendChild(script); return promise; }; // 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, { configurable: false, enumerable: true, get: getter }); } }; // 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 = “”; // on error function for async loading webpack_require.oe = function(err) { console.error(err); throw err; }; // Load entry module and return exports return webpack_require(webpack_require.s = 0);})/*****************************************************************/([//存放没有经过异步加载的,随着执行入口文件加载的模块/ 0 /// (function(module, exports, webpack_require) {webpack_require.e/ import() /(0).then(webpack_require.bind(null, 1)).then(show=>{ show(‘Webpack’)})// })]); ...

January 14, 2019 · 6 min · jiezi

webpack引入第三方库的方式,以及注意事项

一般情况下,我们不用担心所使用的第三方库,在npm管理仓库中找不到。如果需要某一个库,如:jquery,可以直接运行npm install jquery脚本命令来安装这个项目所需要的依赖;然后,在使用jquery的模块文件中,通过import $ from ‘jquery’或者var $ = require(‘jquery’)来引入。这是常用的在webpack构建的项目中引入第三方库的方式。注:为了更好的演示示例代码,示例是在nodemon这篇文章的基础上操作的。但是,在不同的场景下,对webpack构建的项目有不同的需求:项目的体积足够小(cdn)如果是webapck的处理方式,可参考webapck——实现构建输出最小这篇文章。使用非webapck的处理方式,如:CDN。操作也很简单,如果使用html-webpack-plugin直接在模板文件template/index.html中引入某个cdn(如:boot CDN)上的某个第三方库(如:jquery),参考代码如下:<!DOCTYPE html><html lang=“en”><head> <meta charset=“UTF-8”> <title>third party</title></head><body> <script src=“https://cdn.bootcss.com/jquery/3.3.1/jquery.min.js"></script></body></html>然后,在module.js中使用jquery即可,参考代码如下:require(’./module.css’);module.exports = function() { $(document.body).append(’<h1>hello webpack</h1>’) }最后,运行npm run test,构建结束后,你会在浏览器中看到hello webpack字样,背景是红色的页面效果。全局环境下使用第三方库(provide-plugin or imports-loader)为了避免每次使用第三方库,都需要用import或者require()引用它们,可以将它们定义为全局的变量。而webpack的ProvidePlugin内置的插件,可以解决这个问题。详情可参考ProvidePlugin这篇文章的介绍。避免于cdn引用的jquery冲突,这里使用lodash。首先,安装lodash依赖,命令如下:yarn add lodash –dev然后,在webpack.config.js中,添加如下代码:new webpack.ProvidePlugin({ _: ’lodash’}),其次,在module.js中添加如下代码:…var arr = [1, 2, 3, 4, 5 ,6];// provide-plugin$(document.body).append(’<h1>’ + .concat(arr, ‘’) + ‘</h1’);…最后,运行npm run test脚本命令,构建完成后,你就可以浏览器的页面中增加了1,2,3,4,5,6,。如果,你想指定lodash的某个工具函数可以全局使用,如:.concat,首先,像下面这样修改webapck.config.js,代码如下:…new webpack.ProvidePlugin({ // _: ’lodash’, concat: [’lodash’, ‘concat’]}),…然后,修改module.js,代码如下:…var arr = [1, 2, 3, 4, 5 ,6];// provide-plugin// $(document.body).append(’<h1>’ + .concat(arr, ‘’) + ‘</h1’);$(document.body).append(’<h1>’ + _concat(arr, ‘’) + ‘</h1’);…如果不喜欢用插件的,也可以考虑使用import-loader,它也可以实现相同的目的。为了避免不必要的干扰,可以使用underscore来演示。首先,安装imports-loader依赖,命令如下:yarn add imports-loader –dev然后,安装underscore依赖,命令如下:yarn add underscore其次,在webapck.config.js中添加如下代码:…module: { rules: [ { test: require.resolve(‘underscore’), use: ‘imports-loader?=underscore’ }, … ]},…注:underscore和lodash都是用的是单例的模式开发的,它们实例化的构造函数的名字都是,为了作区分,需要对其中一个做一下改变。imports-loader对这个标识起别名有点儿困难,而provide-plugin则没有这个问题,可以定一个个性化的别名。修改webpack.config.js,代码如下:new webpack.ProvidePlugin({ // _: ’lodash’, // _concat: [’lodash’, ‘concat’], __: ’lodash’}),可以为lodash定义为__与underscore的_作区分。然后,修改module.js,代码如下:…// provide-plugin// $(document.body).append(’<h1>’ + _.concat(arr, ‘’) + ‘</h1’);// $(document.body).append(’<h1>’ + _concat(arr, ‘’) + ‘</h1’);$(document.body).append(’<h1>’ + __.concat(arr, ‘~’) + ‘</h1’);…最后,保存所有的文件,可以下浏览器中看到相似的结果(保存后,nodemon自启动浏览器)。cdn与externals之前遇到了一些externals的问题,为什么要详细的说,是因为很多人不明白它到底用来干什么的。场景再现:之前,有一个项目使用了jquery,由于这个库的比较经典,它在应用的各个模块中被频繁引用。使用的方式如下:import $ from ‘jquery’或者var $ = require(‘jquery’)结果是构建结束后,文件比较大。那么考虑使用cdn,如上文描述的那样。这样需要删除import或require的引用,同时删除安装的jquery依赖,但是由于项目结构比较乱,模块比较多,为了避免造成少改或者漏改的问题,会造成应用出错。该怎么办呢?有的人说,不删除jquery依赖,那么使用cdn的目的就没有意义了。而使用external则可以解决这个问题。可以在module.js文件中添加如下代码:…var $ = require(‘jquery’)…然后,保存文件,发现构建输出提示如下的错误:ERROR in ./module.jsModule not found: Error: Can’t resolve ‘jquery’ in ‘E:\workspace\me\webpack-play\demo\example-1’ @ ./module.js 3:0-23 @ ./main.js @ multi (webpack)-dev-server/client?http://localhost:8080 ./main.js模块module.js中的jquery不能被解析。紧接着,在webpack.config.js中添加如下代码:externals: { jquery: ‘jQuery’, jquery: ‘$’},其中jquery代表的是require(‘jquery’)中的jquery,而jQuery和$代表的是jquery这个库自身提供的可是实例化的标识符。其它的库的cdn化,修改类似jquery。但是,如果在项目一开始就决定用cdn的话,就不要在使用jquery的模块中,使用var $ = require(‘jquery’) 或 import $ from ‘jquery’;,虽然这样做不会报错,但是如果出于某方面的考虑,后期可能会引入jquery依赖,那么就必须使用var $ = require(‘jquery’) 或 import $ from ‘jquery’;。参考源代码 ...

January 14, 2019 · 1 min · jiezi

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

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

January 14, 2019 · 1 min · jiezi

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

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

January 13, 2019 · 2 min · jiezi

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

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

January 13, 2019 · 2 min · jiezi

一个基于react+webpack的多页面应用配置

简单介绍首先本文不会对webpack代码进行解释,其所有配置都可以在文档上找到。平时工作中会写一些多页面应用,因为习惯了react的开发模式,故此写了一个简单的配置,跟大家一起分享。如果你也喜欢,对你的开发有所帮助,希望给点鼓励(start)github地址:https://github.com/ivan-GM/Gm…项目目录介绍:打包后文件目录:打包成cli如果你厌烦了新项目的复制、粘贴,也可以构建成cli1,首先创建个文件夹,npm init初始化项目;2, 创建bin目录,touch index.js 编写配置文件;const commander = require(‘commander’);const inquirer = require(‘inquirer’);const download = require(‘download-git-repo’)const ora = require(‘ora’);const questions = [ { type: ‘input’, name: ‘projectName’, message: ‘project name:’, filter: function (val) { return val; } }]commander .option(‘init’, ‘create project’) .version(‘1.0’, ‘-v, –version’)commander .command(‘init’) .description(’’) .action(() => { inquirer.prompt(questions).then(answers => { const { projectName } = answers; const spinner = ora(‘Loading unicorns’).start(); spinner.color = ‘green’; spinner.text = ‘downloading template…’; download(‘direct:https://github.com/ivan-GM/live’, projectName, { clone: true }, (err) => { if (err) { console.log(err) } else { spinner.stop() console.log(‘sucess’) } }) }) });commander.parse(process.argv);3,添加命令:打开package.json “bin”: { “my-cli”: “./bin/index.js” },4,发布npm*上面代码只是对打包成cli进行了简单的说明,如果感兴趣了,可以深入研究 ...

January 13, 2019 · 1 min · jiezi

webpack打包性能优化之路

性能优化的路没有穷尽,只有更快。打开页面越快越好,点击响应越快越好。在当今这个以快为主的时代,快才是王道。闲话扯完,说正事!!!该优化方案以最近做的一个hybrid webapp为实例演示。路由懒加载(1)vue-router文件中的router使用懒加载方式。如下图所示(2)在vue文件中,也采用类似方式引入其他vue组件const showImage = () => import(’@/components/common/showImage’);这个优化的方式在vue官网也有介绍,传送门启用gzip压缩和关闭sourcemap所有现代浏览器都支持 gzip 压缩并会为所有 HTTP 请求自动协商此类压缩。启用 gzip 压缩可大幅缩减所传输的响应的大小(最多可缩减90%),从而显著缩短下载相应资源所需的时间、减少客户端的流量消耗并加快网页的首次呈现速度。 如下图所示如果你使用的是vue-cli2生成的项目的话,在config文件夹下的index.js中可以找到这段代码。记得开启gzip压缩前要安装一个插件,如途中注释掉的一段代码所示。生产环境去掉console代码,减少代码体积,使用uglifyjs压缩代码图片优化对于网页来说,在所下载的字节数中,图片往往会占很大比例。因此,优化图片通常可以卓有成效地减少字节数和改进性能:浏览器需要下载的字节数越少,对客户端带宽的争用就越少,浏览器下载内容并在屏幕上呈现内容的速度就越快。 尽量减少图片的使用,或者使用css3来代替图片效果。如果不行的话,小图片通过一定的工具合成雪碧图或者转成base64。引用的库尽量按需加载。(1)像一般的ui库element,vant等库都提供来按需加载的方式,避免全部引入,加大项目体积。(2)以cdn方式载入需要的库,也可以减少打包后的体积。在index.html文件中引入mintui<!– 引入样式 –><link rel=“stylesheet” href=“https://unpkg.com/mint-ui/lib/style.css"><!-- 引入组件库 –><script src=“https://unpkg.com/mint-ui/lib/index.js"></script>引入vue<!– 开发环境使用此方案–><script src=“https://cdn.jsdelivr.net/npm/vue@2.5.21/dist/vue.js"></script><!-- 生产环境使用此方案 –><script src=“https://cdn.jsdelivr.net/npm/vue@2.5.21/dist/vue.min.js"></script>以这种外链方式引入mint-ui和vue后,需要做些别的配置 (1)在入口文件main.js 中就不需要引入vue和mintui了 (2)在buildwebpack.base.conf.js中添加如下配置,意为打包的时候不打包vue和mint-ui。externals:{ “mint-ui”:“mintui”, “vue”:“Vue”},使用DllReferencePlugin将平时不经常变动的文件抽离出来,统一打包,这样也可以减少后续打包的时间。在build文件夹中新建一个webpack.dll.conf.js.const path = require(‘path’)const webpack = require(‘webpack’)const OptimizeCSSAssetsPlugin = require(‘optimize-css-assets-webpack-plugin’)const UglifyJsPlugin = require(‘uglifyjs-webpack-plugin’)module.exports = { mode: process.env.NODE_ENV === ‘production’ ? ‘production’ : ‘development’, entry: { vendor: [ //根据实际情况添加 ‘axios’, ‘vue/dist/vue.min.js’, ‘vue-router’, ‘vuex’, ‘mint-ui’ ] }, output: { path: path.resolve(__dirname, ‘../static/js’), filename: ‘[name].dll.js’, library: ‘[name]_library’ }, module: { rules: [ { test: /.vue$/, loader: ‘vue-loader’ }, { test: /.js$/, loader: ‘babel-loader’, exclude: /node_modules/(?!(autotrack|dom-utils))/ } ] }, optimization: { minimizer: [ new UglifyJsPlugin({ cache: true, parallel: true, sourceMap: false // set to true if you want JS source maps }), // Compress extracted CSS. We are using this plugin so that possible // duplicated CSS from different components can be deduped. new OptimizeCSSAssetsPlugin({}) ] }, plugins: [ /* @desc: https://webpack.js.org/plugins/module-concatenation-plugin/ “作用域提升(scope hoisting)",使代码体积更小函数申明会产生大量代码 */ new webpack.optimize.ModuleConcatenationPlugin(), new webpack.DllPlugin({ path: path.join(__dirname, ‘.’, ‘[name]-manifest.json’), name: ‘[name]_library’ }) ]}在package.json中增加配置"scripts”: { “build:dll”: “webpack -p –progress –config build/webpack.dll.conf.js” }执行npm run build:dll命令就可以在根目录下生成vendor-manifest.json,static/js下生成vendor.dll.js在webpack.base.conf.js中增加如下const manifest = require(’../vendor-manifest.json’)….plugins: [ //把dll的vendor-manifest.json引用到需要的预编译的依赖 new webpack.DllReferencePlugin({ manifest })]在index.html底部添加<script src=”./static/js/vendor.dll.js”></script>目前在项目中做的优化就是这些,还是那句话,性能优化的路没有穷尽,只有更快。参考文章(1)https://blog.csdn.net/blueber… (2)https://developers.google.com… (3)https://www.jeffjade.com/2017… ...

January 12, 2019 · 1 min · jiezi

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

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

January 12, 2019 · 1 min · jiezi

webpack最小化lodash

lodash作为一个比较常用的前端开发工具集,在使用webpack进行vendor分离的实践中,会遇到将整个lodash文件分离到vendor.js的问题。这样会使vendor.js文件变得特别大。webpack.config.js文件代码如下:var path = require(‘path’);module.exports = mode => { return { entry: { A: ‘./moduleA.js’, B: ‘./moduleB.js’, C: ‘./moduleC.js’, }, mode: mode, output: { path: path.resolve(__dirname, ‘dist/’), filename: ‘[name].js’ }, optimization: { usedExports: true, splitChunks: { cacheGroups: { commons: { chunks: ‘all’, minChunks: 2, maxInitialRequests: 5, minSize: 0 }, vendor: { test: /node_modules/, chunks: “initial”, name: “vendor”, priority: 10, enforce: true } } } }, module: { }, plugins: [ ] }}运行npm run test脚本命令,结果如下:Hash: 5d86af7ed04c57cca071Version: webpack 4.28.4Time: 5707msBuilt at: 2019-01-11 19:25:04 Asset Size Chunks Chunk Names A.js 1.46 KiB 3 [emitted] A B.js 1.53 KiB 4 [emitted] B C.js 1.54 KiB 5 [emitted] CcommonsABC.js 132 bytes 0 [emitted] commonsABC commonsAC.js 238 bytes 1 [emitted] commonsAC vendor.js 69.7 KiB 2 [emitted] vendorEntrypoint A = vendor.js commonsABC.js commonsAC.js A.jsEntrypoint B = commonsABC.js B.jsEntrypoint C = vendor.js commonsABC.js commonsAC.js C.js如上面的情况,vendor.js文件为69.7kb,如果再引用了其他第三方库,文件会更大。那么,如何在开发的过程中,压缩lodash呢?babel-loader & babel-plugin-lodash可以使用babel-loader在对*.js文件进行解析,然后借助于babel-plugin-lodash插件对引用的lodash进行类似tree shaking的操作,这样就可以去除未使用的lodash代码片段。安装所需依赖:yarn add babel-loader @babel/core @babel/preset-env babel-plugin-lodash –dev像下面这样修改webpack.config.js配置文件:…module: { rules: [ { test: /.js$/, exclude: /node_modules/, use: { loader: ‘babel-loader’, options: { presets: [’@babel/preset-env’], plugins: [’lodash’] } } } ]}…运行npm run test,脚本命令结果如下:Hash: 30def5521978552cc93dVersion: webpack 4.28.4Time: 3249msBuilt at: 2019-01-11 21:25:23 Asset Size Chunks Chunk Names A.js 1.46 KiB 3 [emitted] A B.js 1.53 KiB 4 [emitted] B C.js 1.54 KiB 5 [emitted] CcommonsABC.js 132 bytes 0 [emitted] commonsABC commonsAC.js 226 bytes 1 [emitted] commonsAC vendor.js 502 bytes 2 [emitted] vendorEntrypoint A = vendor.js commonsABC.js commonsAC.js A.jsEntrypoint B = commonsABC.js B.jsEntrypoint C = vendor.js commonsABC.js commonsAC.js C.jsvendor.js文件从69.7kb降至502bytes。根据babel-plugin-lodash参考文档介绍,使用lodash-webpack-plugin可以进一步压缩lodash。lodash-webpack-plugin安装lodash-webpack-plugin依赖:yarn add lodash-webpack-plugin –dev修改webpack.config.js配置文件如下:var LodashModuleReplacementPlugin = require(’lodash-webpack-plugin’);…plugins: [ new LodashModuleReplacementPlugin,]…运行npm run test脚本命令,结果如下所示:Hash: 30def5521978552cc93dVersion: webpack 4.28.4Time: 2481msBuilt at: 2019-01-11 21:07:23 Asset Size Chunks Chunk Names A.js 1.46 KiB 3 [emitted] A B.js 1.53 KiB 4 [emitted] B C.js 1.54 KiB 5 [emitted] CcommonsABC.js 132 bytes 0 [emitted] commonsABC commonsAC.js 226 bytes 1 [emitted] commonsAC vendor.js 502 bytes 2 [emitted] vendorEntrypoint A = vendor.js commonsABC.js commonsAC.js A.jsEntrypoint B = commonsABC.js B.jsEntrypoint C = vendor.js commonsABC.js commonsA~C.js C.jsvendor.js依然是502 bytes,问题不在loadsh-webpack-plugin,它虽然会进一步优化lodash,但是在无法进一步优化的情况下,它也没办法。一般情况下,不使用lodash-webpack-plugin就可以满足开发的需要,但是文件特别大的情况下,建议还是使用它。参考源代码 ...

January 11, 2019 · 2 min · jiezi

React-CRA 多页面配置(react-app-rewired)

更新时间:2019-01-11版本信息:CRA v2.1.1 + Webpack v4.19.1 + react-app-rewired v1.6.2一、前言为什么要进行多页面配置在使用 React 进行开发的过程中,我们通常会使用 creat-react-app 脚手架命令来搭建项目,避免要自己配置 webpack,提高我们的开发效率。但是使用 creat-react-app 搭建的项目是单页面应用,如果我们是做中后台管理页面或 SPA,这样是满足要求的,但如果项目有多入口的需求,就需要我们进行一些配置方面的修改。一般现在有两种方式将脚手架搭建的项目修改为多入口编译:执行 npm eject 命令,弹出配置文件,进行自定义配置。请参见:React-CRA 多页面配置(npm eject)。使用 react-app-rewired 修改脚手架配置,在项目中安装 react-app-rewired 后,可以通过创建一个 config-overrides.js 文件来对 webpack 配置进行扩展。本文对第 2 种方法给出具体配置方案,第 1 种方案详见:React-CRA 多页面配置(npm eject)。在阅读本文之前,可以先了解一下第 1 种方案,有助于更好的理解本文内容。webpack 基础本文对 React 多页面应用配置的探讨是基于使用 create-react-app 脚手架命令构建的项目,并不是 webpack 多页面配置教程。但上述两种方案都应该具有一定的 webpack 的基础,实际上当你决定要改造或增强项目的构建打包配置的时候,你应该先对 webpack 进行一定的了解。如果你之前使用 webpack v3.x 版本,这里附上 webpack v4.0.0 更新日志 以及 webpack4升级完全指南。方案说明通过使用 react-app-rewired 可以修改脚手架配置,从而实现扩展 webpack 配置,如增加对 less 文件的支持、增加 antd 组件的按需加载、处理 html 文档中的图片路径问题等,甚至可以将单页面入口编译修改为多页面入口编译的方式。但是需要说明的是,我们可以通过 react-app-rewired 在不暴露配置文件的情况下达到扩展项目配置的目的,但并不建议使用这种方式进行多页面入口编译的配置,虽然本文主要就是介绍这种方案。本文的意义更多的是记录对这种方案的尝试,如果真的需要进行多页面配置,最好还是使用 npm eject 暴露配置文件的方式。主要原因是,通过 react-app-rewired 来实现多页面配置,是需要对脚手架原来的配置具有一定了解的,相较于 npm eject 暴露配置文件的方式来说,这种方式是不太具有透明度的,后面维护的难度较大。本文方案完成的基础是,我之前已经通过 npm eject 这种方式改造过一次多页面的配置,相当于说我已经拆过这个箱子了,我基本上了解了这个箱子里有什么,因此我在配置这个方案的过程中其实是对照着 npm eject 这个方案中的配置文件来逐步进行的。版本的变动使用 CRA 脚手架命令生成的项目免去了我们自己配置 webpack 的麻烦,其内置的各种配置所需要的插件和依赖包的版本都是确定的,是经过了检验的成熟配置,不会因为其中某个依赖包版本的问题造成构建出错。但当我们决定要自己动手配置 webpack 的时候,就意味着我们要自己根据需要安装一些 plugin 或 npm 依赖包,而这些插件和依赖包的版本可能不适用于当前 CRA 的版本,从而造成构建过程中出现一些不可预期的错误。因此,我们需要查看 CRA 脚手架项目的 package.json 文件,对于其中已经列出的 dependencies 依赖包,我们不应该改变这些依赖包的版本,而对于未列出的 npm 包,我们在使用的过程中需要逐个验证,不要一次性安装很多个 npm 包,否则执行构建命令的时候如果出错就会很难排查,最好是根据我们需要的功能逐个的安装相应的 npm 包,确定没有问题,再进行下一个功能的扩展。正是由于版本的变动会对配置产生很大的影响,因此当我们确定了一个配置方案之后,不要再轻易去改动其中涉及到的 npm 包的版本,通过 package-lock.json 文件锁定版本,防止配置方案错乱。另外,在本文编辑的时候,CRA 的最新版本为 v2.1.2,这个版本的 CRA 生成的项目,当前版本的 react-app-rewired v1.6.2 已经无法使用,具体信息可以参见 CRA >=2.1.2 breaking issue 2.1.2。至少在本文编辑的时候(2019.01.05),是无法适用于 CRA >=2.1.2 版本的,因此本文方案是基于最后一个可以适用的 CRA v2.1.1 版本来做的,package.json 文件中其他的 version 信息会附在方案后面。2019-01-11 补充:上面提到的版本基本都是指 package.json 文件中列出的依赖包的版本,但是严格来讲还应包含一些构建配置中使用的 node.js 工具包,比如 globby、dir-glob等,这些工具包的版本更新也有可能会造成构建出错。这种情况的出现往往是无法预期的,它们造成的影响一般比较广泛,而不仅仅是出现在我们方案配置的过程中,这种错误基本上都会在 Github 上有相应的 issue 以及解决方法或修复措施,本文中也列出了遇到的一个这种类型的错误,详见 四、错误排查 章节中的 其他错误 一节。二、准备工作创建一个 CRA v2.1.1 项目我们的配置方案是基于 CRA v2.1.1 脚手架项目进行改造的,因此首先我们要先创建一个 CRA 项目,详见官方文档:Create React App。但是这样创建出来的项目默认是最新版本的 CRA 项目,我们在上文中已经说明,我们的配置方案是要求特定版本的 CRA 项目的,那么如何创建特定的 CRA v2.1.1 版本项目?CRA 项目版本的核心其实就是 react-scripts 的版本,我们可以先创建一个最新版本的脚手架项目,然后更改为 v2.1.1 版本,具体如下:创建一个最新版本的 CRA 项目,参见官方文档:Create React App;删除 node_modules 文件夹,如果有 package-lock.json 文件或 yarn.lock 文件,也要一并删除,否则重新安装 node_modules 依赖包仍然会被锁定为原来的版本;修改 package.json 文件,重新指定 react-scripts 的版本:“dependencies”: { - “react”: “^16.7.0”, + “react”: “^16.6.3”, - “react-dom”: “^16.7.0”, + “react-dom”: “^16.6.3”, - “react-scripts”: “2.1.3” + “react-scripts”: “2.1.1”}执行 yarn install 或 npm install 重新安装项目依赖。至此,我们已经创建了一个 CRA v2.1.1 项目,这将作为我们进行多页面入口编译改造的基础。react-app-rewired 的用法首先要学习 react-app-rewired 的用法,参照官方文档:How to rewire your create-react-app project。主要是三点:安装 react-app-rewired创建一个 config-overrides.js 文件修改 package.json 文件中的脚本命令查看 CRA 项目原有的配置文件上面我们提到在进行多页面配置之前,需要对脚手架原有的配置文件具有一定了解,那么如何查看原有的配置文件?方法1:将使用 CRA 脚手架命令生成的项目拷贝一份,执行 npm eject 暴露配置文件,eject 之后文件组织结构如下:my-app├── config│ ├── jest│ ├── env.js│ ├── paths.js│ ├── webpack.config.dev.js│ ├── webpack.config.prod.js│ └── webpackDevServer.config.js├── node_modules├── public├── scripts│ ├── build.js│ ├── start.js│ └── test.js├── package.json├── README.md└── src其中 config 文件夹下就是脚手架原有的配置文件。方法2:使用 CRA 脚手架命令生成项目,在 my-app/node_modules/react-scripts/config 路径下可以看到原有的配置文件方法3:在 config-overrides.js 文件中将 webpack 配置对象输出在控制台,查看其结构推荐使用第 1 种方式,便于我们在配置过程中查看和操作,第 3 种方式作为一种辅助手段,主要是用于改造过程中的调试和验证配置对象的改造结果的。项目文件组织结构在开始进行多入口配置之前,需要先明确项目的文件组织结构,这关系到我们如何准确获取所有的入口文件,这里不再详述,请参见React-CRA 多页面配置(npm eject)中的项目文件组织结构一节。三、具体方案安装 react-app-rewired,创建 config-overrides.js 文件,修改 package.json 文件中的脚本命令。详见官方文档 How to rewire your create-react-app project。执行 yarn start 命令,查看 http://localhost:3000,确保安装 react-app-rewired 操作没有问题。修改文件组织结构。这里不再详述,参见上文 项目文件组织结构 一节。CRA项目原文件组织结构为: my-app ├── README.md ├── node_modules ├── package.json ├── config-overrides.js // 安装 react-app-rewired 时创建的 ├── .gitignore ├── public │ ├── favicon.ico │ ├── index.html │ └── manifest.json └── src ├── App.css ├── App.js ├── App.test.js ├── index.css ├── index.js ├── logo.svg └── serviceWorker.js修改为多页面入口编译的文件组织结构: // 本方案示例项目有两个页面 index.html & admin.html // 这里给出的文件组织结构是配置完成后的完整结构, 有些文件(如 src/setupProxy.js)的具体作用后面会给出说明 my-app ├── README.md ├── node_modules ├── package.json ├── config-overrides.js // 安装 react-app-rewired 时创建的 ├── .gitignore ├── public │ ├── favicon.ico │ ├── index.html // 作为所有页面的 html 模板文件 │ └── manifest.json └── src ├── index.js // 空白文件, 为了避免构建报错, 详见下文 ├── setupProxy.js // proxy 设置, 详见下文(在当前操作步骤中可以缺失) ├── index // index.html 页面对应的文件夹 │ ├── App.less │ ├── App.js │ ├── App.test.js │ ├── index.less // 使用 less 编写样式文件 │ ├── index.js │ ├── logo.svg │ └── serviceWorker.js └── admin // admin.html 页面对应的文件夹 ├── App.less ├── App.js ├── App.test.js ├── index.less // 使用 less 编写样式文件 ├── index.js ├── logo.svg └── serviceWorker.js执行 yarn start 命令,查看 http://localhost:3000/index.html 和 http://localhost:3000/admin.html,确保修改文件组织结构操作没有问题。这个示例项目是以 /my-app/public/index.html 作为所有页面的 html 模板文件的,当然也可以分别指定不同的 html 模板文件,这是根据项目需要和项目文件组织结构决定的。在这个示例项目中,由于作为模板的 html 文件只需要有个根元素即可,因此将其作为所有入口的 html 模板文件。这样的话,每个页面的 <title></title> 就需要在各自页面中分别指定,一般可以在页面挂载之后进行操作,比如: class App extends Component { componentDidMount() { document.title = ‘xxx’; } render() { return ( … ); } }修改 config-overrides.js 文件,进行具体配置。实际上我们之后所有的操作都是在这个文件中进行的。我们的测试方案是一个使用了 Ant Design、Redux、Less、Echarts 的多页面项目,需要达到以下要求:指定页面的多入口文件路径以及入口文件对应的 html 模板实现 antd 组件的按需加载增加对 less 文件的支持更改输出的文件名增加对 html 文档中图片路径的处理更改代码切割的配置设置别名路径上述每一个功能的实现,都需要执行 yarn start 或 yarn build 进行验证,确保操作成功后再进行下一项的配置。这里不再逐步说明配置的步骤,详见下面的代码及注释。最终 config-overrides.js 文件内容如下: /* config-overrides.js / / * @Author: mzhang.eric * @Last Modified time: 2019-01-10 18:37:17 * @Description: 使用 react-app-rewired 扩展和改造 CRA v2.1.1 项目, 基于 webpack v4.19.1 + react-app-rewired v1.6.2 版本 / const rewireLess = require(‘react-app-rewire-less’); const { injectBabelPlugin, paths } = require(‘react-app-rewired’); const HtmlWebpackPlugin = require(‘html-webpack-plugin’); const path = require(‘path’); const fs = require(‘fs’); const globby = require(‘globby’); const appDirectory = fs.realpathSync(process.cwd()); const resolveApp = relativePath => path.resolve(appDirectory, relativePath); // const BundleAnalyzerPlugin = require(‘webpack-bundle-analyzer’).BundleAnalyzerPlugin; module.exports = function override(config, env) { // 使用 babel-plugin-import 按需加载组件 config = injectBabelPlugin( [‘import’, { libraryName: ‘antd’, libraryDirectory: ’es’, style: true }], config, ); // 增加 less 支持 config = rewireLess.withLoaderOptions({ // 解决报错: Inline JavaScript is not enabled. Is it set in your options? javascriptEnabled: true, })(config, env); // 入口文件路径 // const entriesPath = globby.sync([resolveApp(‘src’) + ‘//index.js’]); const entriesPath = globby.sync([resolveApp(‘src’) + ‘//index.js’], {cwd: process.cwd()}); paths.entriesPath = entriesPath; // 获取指定路径下的入口文件 function getEntries(){ const entries = {}; const files = paths.entriesPath; files.forEach(filePath => { let tmp = filePath.split(’/’); let name = tmp[tmp.length - 2]; if(env === ‘production’){ entries[name] = [ filePath, ]; } else { entries[name] = [ require.resolve(‘react-dev-utils/webpackHotDevClient’), filePath, ]; } }); return entries; } // 入口文件对象 const entries = getEntries(); // 配置 HtmlWebpackPlugin 插件, 指定入口文件生成对应的 html 文件 let htmlPlugin; if(env === ‘production’){ htmlPlugin = Object.keys(entries).map(item => { return new HtmlWebpackPlugin({ inject: true, template: paths.appHtml, filename: item + ‘.html’, chunks: [item], minify: { removeComments: true, collapseWhitespace: true, removeRedundantAttributes: true, useShortDoctype: true, removeEmptyAttributes: true, removeStyleLinkTypeAttributes: true, keepClosingSlash: true, minifyJS: true, minifyCSS: true, minifyURLs: true, }, }); }); } else { htmlPlugin = Object.keys(entries).map(item => { return new HtmlWebpackPlugin({ inject: true, template: paths.appHtml, filename: item + ‘.html’, chunks: [item], }); }); } if (env === ‘production’) { for (let i = 0; i < config.plugins.length; i++) { let item = config.plugins[i]; // 更改输出的样式文件名 if (item.constructor.toString().indexOf(‘class MiniCssExtractPlugin’) > -1) { item.options.filename = ‘static/css/[name].css?_v=[contenthash:8]’; item.options.chunkFilename = ‘static/css/[name].chunk.css?_v=[contenthash:8]’; } // SWPrecacheWebpackPlugin: 使用 service workers 缓存项目依赖 if(item.constructor.toString().indexOf(‘function GenerateSW’) > -1){ // 更改输出的文件名 item.config.precacheManifestFilename = ‘precache-manifest.js?_v=[manifestHash]’; } } // 更改生产模式输出的文件名 config.output.filename = ‘static/js/[name].js?_v=[chunkhash:8]’; config.output.chunkFilename = ‘static/js/[name].chunk.js?_v=[chunkhash:8]’; } else { // 更改开发模式输出的文件名 config.output.filename = ‘static/js/[name].js’; config.output.chunkFilename = ‘static/js/[name].chunk.js’; } // 修改入口 config.entry = entries; // 修改 HtmlWebpackPlugin 插件 for (let i = 0; i < config.plugins.length; i++) { let item = config.plugins[i]; if (item.constructor.toString().indexOf(‘class HtmlWebpackPlugin’) > -1) { config.plugins.splice(i, 1); } } config.plugins.push(…htmlPlugin); // 分析打包内容 // config.plugins.push(new BundleAnalyzerPlugin()); // 设置别名路径 config.resolve.alias = { …config.resolve.alias, ‘@src’: paths.appSrc, // 在使用中, 有些 Eslint 规则会报错, 禁用这部分代码的 Eslint 检测即可 }; // 处理 html 文档中图片路径问题 config.module.rules[2].oneOf.push({ test: /.html$/, loader: ‘html-withimg-loader’ }); // 修改 build/static/media/ 路径下的文件名 for (let i = 0; i < config.module.rules[2].oneOf.length; i++) { const item = config.module.rules[2].oneOf[i]; if(!item.options || !item.options.name){ continue; } let str = item.options.name.toString(); if(str.indexOf(‘static/media/[name].[hash:8].[ext]’) > -1){ item.options.name = ‘static/media/[name].[ext]?_v=[hash:8]’; } } // 修改代码切割配置 // 参见 webpack 文档:https://webpack.js.org/plugins/split-chunks-plugin/#optimization-splitchunks config.optimization = { splitChunks: { // 将所有入口点共同使用到的、次数超过 2 次的模块,创建为一个名为 commons 的代码块 // 这种配置方式可能会增大初始的捆绑包,比如有些公共模块在首页其实并未用到,但也会打包进来,会降低首页的加载性能 // 建议将非必需模块使用 import() 的方式动态加载,提升页面的加载速度 // cacheGroups: { // commons: { // name: ‘commons’, // chunks: ‘initial’, // minChunks: 2 // } // } // 将所有使用到的 node_modules 中的模块包打包为 vendors 代码块。(不推荐) // 这种方式可能会产生一个包含所有外部依赖包的较大代码块,建议只包含核心框架和工具函数代码,其他依赖项动态加载 // cacheGroups: { // commons: { // test: /[\/]node_modules[\/]/, // name: ‘vendors’, // chunks: ‘all’ // } // } cacheGroups: { // 通过正则匹配,将 react react-dom echarts 等公共模块拆分为 vendor,可以通过 BundleAnalyzerPlugin 帮助确定拆分哪些模块包 vendor: { test: /[\/]node_modules\/[\/]/, name: ‘vendor’, chunks: ‘all’, // all, async, and initial }, // 将 css|less 文件合并成一个文件, mini-css-extract-plugin 的用法请参见文档:https://www.npmjs.com/package/mini-css-extract-plugin // MiniCssExtractPlugin 会将动态 import 引入的模块的样式文件也分离出去,将这些样式文件合并成一个文件可以提高渲染速度 // 其实如果可以不使用 mini-css-extract-plugin 这个插件,即不分离样式文件,可能更适合本方案,但是我没有找到方法去除这个插件 styles: { name: ‘styles’, test: /.css|less$/, chunks: ‘all’, // merge all the css chunk to one file enforce: true } }, }, }; return config; };四、错误排查这里对方案形成过程中遇到的错误进行记录,这些错误有些是配置过程中必定会出现的,是需要我们进行改造的,也有些错误可能是操作不当主观造成的。客观错误这类错误是进行多页面入口编译改造过程中必定会遇到的,有些是由于项目文件组织结构的改变造成的,也有些是为了扩展项目配置而使用了源 CRA 脚手架项目未提供的 npm 包造成的,前者会出现哪些错误我们是可以明确知道的,后者相对来说是不确切的,但大多都是版本不适用造成的。Could not find a required file.Could not find a required file. Name: index.js Searched in: C:xxxxxxmy-appsrc错误描述:通过 create-react-app 脚手架搭建的项目以 /src/index.js 作为应用入口,当执行构建脚本时如果检测到缺失了这个必要文件,node 进程会退出。但是在我们进行多页面入口改造的时候,src 直接路径下没有 index.js 文件,根据我们的文件组织结构,index.js 文件存在于每个独立页面的子文件夹下,如 /src/index/index.js /src/admin/index.js。解决方法:在 src 直接路径下创建一个空的 index.js 文件。Inline JavaScript is not enabled. Is it set in your options?// https://github.com/ant-design… .bezierEasingMixin();^ Inline JavaScript is not enabled. Is it set in your options? in C:xxxsrcmy-appnode_modulesantdesstylecolorbezierEasing.less (line 110, column 0)错误描述:当我们使用了 antd 组件的时候,构建报错,需要允许 less 文件中 js 的执行。解决方法:在 config-overrides.js 文件中增加 less 文件支持时,设置 javascriptEnabled 为 true。 module.exports = function override(config, env) { … // 增加 less 支持 config = rewireLess.withLoaderOptions({ // 解决报错: Inline JavaScript is not enabled. Is it set in your options? javascriptEnabled: true, })(config, env); … return config; };Failed to load resource: net::ERR_FILE_NOT_FOUND(build 版本)错误描述:执行 yarn build 或 npm run build 构建生产版本时,构建出的页面未能正确加载样式和脚本文件,chrome 检查工具报路径错误。解决方法:修改 package.json 文件,指定 homepage 字段的值,本项目这里指定为相对路径。 “homepage”: “./",When specified, “proxy” in package.json must be a string.When specified, “proxy” in package.json must be a string. Instead, the type of “proxy” was “object”. Either remove “proxy” from package.json, or make it a string.错误描述:我们在开发过程中一般会在 package.json 文件中配置 proxy 代理服务器,但是在 CRA 2.x 升级以后对 proxy 的设置做了修改,具体请参见官方升级文档:Move advanced proxy configuration to src/setupProxy.js解决方法:移除 package.json 文件中有关 proxy 的设置,使用 http-proxy-middleware,在 src 目录下创建 setupProxy.js 文件。详细方法请参见上述文档。主观错误这类错误应该是可以避免的,但是在配置过程中可能会由于开发人员的不当操作造成出错,最主要的就是安装了错误版本的 plugin 或 npm 包,我们上面已经说过,对于项目原本的配置中在 package.json 文件中已经列出的 plugin 和 npm 包,不要再重复进行安装,否则可能重新安装的 plugin 或 npm 包版本是不适用的,从而造成出错。例如,CRA v2.1.1 脚手架项目原有的 html-webpack-plugin 版本是 v4.0.0-alpha.2,是不需要再另外进行安装的。如果此时开发人员自己执行了 yarn add html-webpack-plugin,从而将 html-webpack-plugin 的版本更换为了 v3.2.0,这就会造成构建报错:URIError: Failed to decode param ‘/%PUBLIC_URL%/favicon.ico’ URIError: Failed to decode param ‘/%PUBLIC_URL%/manifest.json’,其他主观错误类似。html-webpack-plugin 版本错误URIError: Failed to decode param ‘/%PUBLIC_URL%/favicon.ico’ URIError: Failed to decode param ‘/%PUBLIC_URL%/manifest.json’错误描述:在执行 yarn start 构建命令时报错,页面空白。解决方法:html-webpack-plugin 版本错误,在本文的配置方案中,应当使用 “html-webpack-plugin”: “4.0.0-alpha.2”, 版本。TypeError: Cannot read property ‘state’ of undefined(页面报错)错误描述:编译构建过程没有报错,但页面报错:TypeError: Cannot read property ‘state’ of undefined。解决方法:redux 版本错误,在本文的配置方案中,应当使用 redux <=3.7.2 版本。其他错误我们在上文 版本的变动 一节的补充中已经对此有所提及,这类错误主要是由 node.js 工具包版本的升级造成的,这些错误基本都是不可预期的,也无法在这里全部涵盖,只能就当前遇到的问题进行简要记录,可能随着时间的推移,还会出现其他的类似问题,也可能这些错误已经在后续的版本中被修复了,因此请勿纠结于这里记录的错误,如果遇到了这类错误,就查阅资料进行修正,如果没有遇到,则无须理会。TypeError: Expected cwd to be of type string but received type undefinedC:xxxmy-appnode_modulesdir-globindex.js:59 throw new TypeError(Expected \cwdto be of typestringbut received type${typeof opts.cwd}``); TypeError: Expected cwd to be of type string but received type undefined错误描述:本文的写作开始于 2019-01-05,在 2019-01-11 重新审核本文方案的时候,遇到了这个错误,主要是由于 dir-glob 版本的升级造成的,我们在配置脚本中使用了 globby 的 sync 方法,dir-glob 版本升级之后,这个方法的调用会使得 dir-glob 抛出上述错误。详细信息参见:Broken build do to major change from 2.0 to 2.2 以及 globby will pass opts.cwd = undefined to dir-glob, which leads to TypeError.。解决方法:这里给出的解决方法是限定于当前时间的,因为在本文编辑的时候(2019-01-11)这个 issue 还没有给出最终的解决方案,个人觉得可能会由 globby 进行修复。 / config-overrides.js / // 修改获取入口文件路径的代码 - const entriesPath = globby.sync([resolveApp(‘src’) + ‘//index.js’]); + const entriesPath = globby.sync([resolveApp(‘src’) + ‘//index.js’], {cwd: process.cwd()});五、package.json 信息本文的多页面配置方案是基于 CRA 脚手架项目的,项目的依赖包信息会包含在两个 package.json 文件中,其中脚手架自带的配置信息位于 /my-app/node_modules/react-scripts/package.json 文件中,而我们根据项目需要自己增加的配置信息在 /my-app/package.json 文件中,这里将本方案项目配置信息附录如下: / package.json */ { “name”: “my-app”, “version”: “0.1.0”, “private”: true, “dependencies”: { “antd”: “^3.12.1”, “babel-plugin-import”: “^1.11.0”, “echarts”: “^4.2.0-rc.2”, “echarts-for-react”: “^2.0.15-beta.0”, “html-withimg-loader”: “^0.1.16”, “http-proxy-middleware”: “^0.19.1”, “react”: “^16.6.3”, “react-app-rewire-less”: “^2.1.3”, “react-app-rewired”: “^1.6.2”, “react-dom”: “^16.6.3”, “react-intl”: “^2.7.2”, “react-lazyload”: “^2.3.0”, “react-loadable”: “^5.5.0”, “react-redux”: “^6.0.0”, “react-scripts”: “2.1.1”, “redux”: “3.7.2”, “redux-promise-middleware”: “^5.1.1”, “webpack-bundle-analyzer”: “^3.0.3” }, “scripts”: { “start”: “react-app-rewired start”, “build”: “react-app-rewired build”, “test”: “react-app-rewired test –env=jsdom”, “eject”: “react-scripts eject” }, “eslintConfig”: { “extends”: “react-app” }, “browserslist”: [ “>0.2%”, “not dead”, “not ie <= 11”, “not op_mini all” ], “homepage”: “./” } ...

January 11, 2019 · 8 min · jiezi

webpack4 单独抽离打包 css 的新实现

webpack4 单独抽离打包 css 的新实现前言之前我们使用的打包 css 无非两种方式:① 将 css 代码打包进 入口 js 文件中;② 使用第三方插件(extract-text-webpack-plugin)实现【注意,该插件在 webpack4 中已经不推荐使用,而且会出现各种莫名其妙的 bug】正是基于对以上两种方式缺点的思考,结合我的实际使用过程,我认为以后我们应该完全摒弃掉上述两种方式,这里推荐一种一种新的实现方式:file-loaderfile-loader我先给个 file-loader 的使用说明吧(传送门:https://github.com/webpack-co…;在我们的传统认知中 file-loader 大多是用来处理 图像元素的,其实如果你认真看过上面的那个传送门的话,才明白我们一直以来是被被误导了,下面我给出几个官方的使用例子吧传统的处理图像module.exports = { module: { rules: [ { test: /.(png|jpg|gif)$/, use: [ { loader: ‘file-loader’, options: {}, }, ], }, ], },};处理 css 【本文重点】const path = require(‘path’);const CleanWebpackPlugin = require(‘clean-webpack-plugin’);const HtmlWebpackPlugin = require(‘html-webpack-plugin’);module.exports = { entry: { app: ‘./src1/index.js’, print: ‘./src1/print.js’ }, output: { filename: ‘[name].bundle.js’, path: path.resolve(__dirname, ‘dist’) }, module: { rules: [ { // loader 图片 test: /.(png|svg|jpg|gif)$/, use: [‘file-loader’] }, { // 处理字体 test: /.(woff|woff2|eot|ttf|otf)$/, use: [ ‘file-loader’ ] }, { // 单独打包出 css test: /.css$/, use: [‘file-loader’] } ] }, plugins: [ new CleanWebpackPlugin([‘dist’]), new HtmlWebpackPlugin({ title: ‘webpack4 extract css methord of new’, template: ‘./src1/index.html’, filename: ‘index.html’, minify: { collapseWhitespace: true }, hash: true }) ], mode: “production”};上面这个代码是我亲自实测过的,大家可以放心借鉴,使用了 file-loader 来处理 css 后,就不再需要额外的插件了,当然了,如果你需要指定输出的 css 文件名和路径,可以这么写 user: [‘file-loader?name=[name].bundle[hash].css’]等,这里的写法就是类似 get 方式的传参那样,?符号后面带参数名即可仅供参考,如果你有什么更好的建议可以 contact 我哦 ...

January 11, 2019 · 1 min · jiezi

webpack -- require和import机制

欢迎访问我的个人博客:http://www.xiaolongwu.cn前言虽然我们很多人每天都在写项目,require或者import写的爽得很,但还是有很大一部分人不清楚它背后的运行原理和所谓的规则机制。开始我们基于webpack开发,就拿基本的vue项目来举例子吧假如我们项目中要用到vue或者express框架,我们的代码就这样写import Vue from ‘vue’//或者var Vue = require(‘vue’)然后我们就能在下面轻松的用Vue这个变量,感觉很愉悦,但是你想过我们是怎么拿到Vue这个东西的吗?我们写的import或者require这行代码道理干了啥?首先,import是es2015的模块引入规范,而require是commonjs的模块引入规范;webpack支持es2015,commonjs,AMD等规范;工作机制前提是你在做web开发,试图用webpack或者rollup打包你的项目;首先会从本地的node_modules文件夹中找到vue文件夹,看是否存在package.json文件;如果找到了package.json,就会先找module字段,然后读取对应的路径下的文件,查找到此结束;如果没找到module字段,就会找main字段,然后读取对应的路径下的文件,查找到此结束;如果没有main字段,就会在vue文件夹下找index.js文件,然后读取文件,查找到此结束;如果以上都没找到就会返回异常,扔出not find异常如果不存在package.json就会找index.js文件,然后读取文件,查找到此结束;如果还没有就会抛出异常;简单说一下module字段说到module字段就不得不说一个和webpack很像的模块打包工具—rollup,rollup是一个轻量级的打包工具,一般被用来打包模块或者库,可以根据需要将模块打包为es,commonjs,AMD,CMD,UMD,IIFE等规范的模块;而webpack一般被用来打包应用程序;rollup提出了module这个字段,其原因是一般主流的模块或者库都是commonjs规范的模块,而es2015的模块规范才是js的未来,才应该是主流;所以,一般的package.json中的module对应的模块为es模块,而main对应的为commonjs模块,webpack和rollup都会默认优先读取module字段;github资源地址:webpack–require和import机制.md我的CSDN博客地址:https://blog.csdn.net/wxl1555如果您对我的博客内容有疑惑或质疑的地方,请在下方评论区留言,或邮件给我,共同学习进步。邮箱:wuxiaolong802@163.com

January 11, 2019 · 1 min · jiezi

node服务端渲染(完整demo)

简介nodejs搭建多页面服务端渲染技术点koa 搭建服务koa-router 创建页面路由nunjucks 模板引擎组合htmlwebpack打包多页面node端异步请求服务端日志打印项目源码 git clone https://gitee.com/wjj0720/nod…运行npm inpm start一、 现代服务端渲染的由来服务端渲染概念: 是指,浏览器向服务器发出请求页面,服务端将准备好的模板和数据组装成完整的HTML返回给浏览器展示1、前端后端分离早在七八年前,几乎所有网站都使用 ASP、Java、PHP做后端渲染,随着网络的加快,客户端性能提高以及js本身的性能提高,我们开始往客户端增加更多的功能逻辑和交互,前端不再是简单的html+css更多的是交互,前端页在这是从后端分离出来「前后端正式分家」2、客户端渲染随着ajax技术的普及以及前端框架的崛起(jq、Angular、React、Vue) 框架的崛起,开始转向了前端渲染,使用 JS 来渲染页面大部分内容达到局部刷新的作用优势局部刷新,用户体验优富交互节约服务器成本缺点不利于SEO(爬虫无法爬取ajax)请求回来的数据受浏览器性能限制、增加手机端的耗电首屏渲染需要等js运行才能展示数据3、现在服务端渲染为了解决上面客户端渲染的缺点,然前后端分离后必不能合,如果要把前后端部门合并,拆掉的肯定是前端部门现在服务端渲染的特点前端开发人员编写html+css模板node中间服务负责前端模板和后台数据的组合数据依然由java等前服务端语言提供优势前后端分工明确SEO问题解决4、前、后端渲染相关讨论参考知乎问答:为什么现在又流行服务器端渲染html精读前后端渲染之争服务端渲染 vs 客户端渲染二、 项目开始确保你安装node第一步 让服务跑起来目标: 创建node服务,通过浏览器访问,返回’hello node!’(html页面其实就是一串字符串) /** 创建项目目录结构如下 / │─ package-lock.json │─ package.json │─ README.md ├─bin │─ www.js // 1. 安装依赖 npm i koa // 2. 修改package.json文件中 scripts 属性如下 “scripts”: { “start”: “node bin/www.js” } // 3. www.js写入如下代码 const Koa = require(‘koa’); let app = new Koa(); app.use(ctx => { ctx.body = ‘hello node!’ }); app.listen(3000, () => { console.log(‘服务器启动 http://127.0.0.1:3000’); }); // 4 npm start 浏览器访问 http://127.0.0.1:3000 查看效果第二步 路由的使用目标:使用koa-router根据不同url返回不同页面内容依赖 npm i koa-routerkoa-router 更多细节 请至npm查看 /* 新增routers文件夹 目录结构如下 │─.gitignore │─package.json │─README.md ├─bin │ │─www.js ├─node_modules └─routers │─home.js │─index.js │─user.js / //项目中应按照模块对路由进行划分,示例简单将路由划分为首页(/)和用户页(/user) 在index中将路由集中管理导, 出并在app实例后挂载到app上 /* router/home.js 文件 / // 引包 const homeRouter = require(‘koa-router’)() //创建路由规则 homeRouter.get([’/’, ‘/index.html’, ‘/index’, ‘/home.html’, ‘/home’], (ctx, next) => { ctx.body = ‘home’ }); // 导出路由备用 module.exports = homeRouter /* router/user.js 文件 / const userRouter = require(‘koa-router’)() userRouter.get(’/user’, (ctx, next) => { ctx.body = ‘user’ }); module.exports = userRouter /* router/index.js 文件 / // 路由集中点 const routers = [ require(’./home.js’), require(’./user.js’) ] // 简单封装 module.exports = function (app) { routers.forEach(router => { app.use(router.routes()) }) return routers[0] } /* www.js 文件改写 / // 引入koa const Koa = require(‘koa’) const Routers = require(’../routers/index.js’) // 实例化koa对象 let app = new Koa() // 挂载路由 app.use((new Routers(app)).allowedMethods()) // 监听3000端口 app.listen(3000, () => { console.log(‘服务器启动 http://127.0.0.1:3000’) })第三步 加入模板目标: 1.使用nunjucks解析html模板返回页面 2.了解koa中间件的使用依赖 npm i nunjucksnunjucks中文文档 / *我向项目目录下加入两个准备好的html文件 目录结构如下 │─.gitignore │─package.json │─README.md ├─bin │ │─www.js │─middlewares //新增中间件目录 │ ├─nunjucksMiddleware.js //nunjucks模板中间件 ├─node_modules │─routers │ │─home.js │ │─index.js │ │─user.js │─views //新增目录 作为视图层 ├─home │ ├─home.html ├─user ├─user.html / / nunjucksMiddleware.js 中间件的编写 什么是中间件: 中间件就是在程序执行过程中增加辅助功能 nunjucksMiddleware作用: 给请求上下文加上render方法 将来在路由中使用 / const nunjucks = require(’nunjucks’) const path = require(‘path’) const moment = require(‘moment’) let nunjucksEVN = new nunjucks.Environment(new nunjucks.FileSystemLoader(‘views’)) // 为nkj加入一个过滤器 nunjucksEVN.addFilter(’timeFormate’, (time, formate) => moment(time).format( formate || ‘YYYY-MM-DD HH:mm:ss’)) // 判断文件是否有html后缀 let isHtmlReg = /.html$/ let resolvePath = (params = {}, filePath) => { filePath = isHtmlReg.test(filePath) ? filePath : filePath + (params.suffix || ‘.html’) return path.resolve(params.path || ‘’, filePath) } / * @description nunjucks中间件 添加render到请求上下文 * @param params {} / module.exports = (params) => { return (ctx, next) => { ctx.render = (filePath, renderData = {}) => { ctx.type = ’text/html’ ctx.body = nunjucksEVN.render(resolvePath(params, filePath), Object.assign({}, ctx.state, renderData)) } // 中间件本身执行完成 需要调用next去执行下一步计划 return next() } } / 中间件挂载 www.js中增加部分代码 / // 头部引入文件 const nunjucksMiddleware = require(’../middlewares/nunjucksMiddleware.js’) //在路由之前调用 因为我们的中间件是在路由中使用的 故应该在路由前加到请求上下文ctx中 app.use(nunjucksMiddleware({ // 指定模板文件夹 path: path.resolve(__dirname, ‘../views’) }) / 路由中调用 以routers/home.js 为例 修改代码如下/ const homeRouter = require(‘koa-router’)() homeRouter.get([’/’, ‘/index.html’, ‘/index’, ‘/home.html’, ‘/home’], (ctx, next) => { // 渲染页面的数据 ctx.state.todoList = [ {name: ‘吃饭’, time: ‘2019.1.4 12:00’}, {name: ‘下午茶’, time: ‘2019.1.4 15:10’}, {name: ‘下班’, time: ‘2019.1.4 18:30’} ] // 这里的ctx.render方法就是我们通过nunjucksMiddleware中间件添加的 ctx.render(‘home/home’, { title: ‘首页’ }) }) module.exports = homeRouter第四步 抽取公共模板目标: 抽取页面的公用部分 如导航/底部/html模板等 /views目录下增加两个文件夹_layout(公用模板) _component(公共组件) 目录结构如下 │─.gitignore │─package.json │─README.md ├─bin │ │─www.js /koa服务 │─middlewares //中间件目录 │ ├─nunjucksMiddleware.js //nunjucks模板中间件 ├─node_modules │─routers //服务路由目录 │ │─home.js │ │─index.js │ │─user.js │─views //页面视图层 │─_component │ │─nav.html (公用导航) │─_layout │ │─layout.html (公用html框架) ├─home │ ├─home.html ├─user ├─user.html / <!– layout.html 文件代码 –> <!DOCTYPE html> <html> <head> <meta charset=“UTF-8”> <title>{{ title }}</title> </head> <body> <!– 占位 名称为content的block将放在此处 –> {% block content %} {% endblock %} </body> </html> <!– nav.html 公用导航 –> <ul> <li><a href="/">首页</a></li> <li><a href="/user">用户页</a></li> </ul> <!– home.html 改写 –> <!– njk继承模板 –> {% extends “../_layout/layout.html” %} {% block content %} <!– njk引入公共模块 –> {% include “../_component/nav.html” %} <h1>待办事项</h1> <ul> <!– 过滤器的调用 timeFormate即我们在中间件中给njk加的过滤器 –> {% for item in todoList %} <li>{{item.name}} —> {{item.time | timeFormate}}</li> {% endfor %} </ul> {% endblock %} <!– user.html –> {% extends “../_layout/layout.html” %} {% block content %} {% include “../_component/nav.html” %} 用户中心 {% endblock %}第五步 静态资源处理目标: 处理页面jscssimg等资源引入依赖用webpack打包静态资源 npm i webpack webpack-cli -D处理js npm i @babel/core @babel/preset-env babel-loader -D处理less npm i css-loader less-loader less mini-css-extract-plugin -D处理文件 npm i file-loader copy-webpack-plugin -D处理html npm i html-webpack-plugin -D清理打包文件 npm i clean-webpack-plugin -D> 相关插件使用 查看npm相关文档 / 项目目录 变更 │ .gitignore │ package.json │ README.md ├─bin │ www.js ├─config //增加webpack配置目录 │ webpack.config.js ├─middlewares │ nunjucksMiddleware.js ├─routers │ home.js │ index.js │ user.js ├─src │ │─template.html // + html模板 以此模板为每个入口生成 引入对应js的模板 │ ├─images // +图资源目录 │ │ ww.jpg │ ├─js // + js目录 │ │ ├─home │ │ │ home.js │ │ └─user │ │ user.js │ └─less // + css目录 │ ├─common │ │ common.less │ │ nav.less │ ├─home │ │ home.less │ └─user │ user.less └─views ├─home │ home.html ├─user │ user.html ├─_component │ nav.html └─_layout // webpac打包后的html模板 ├─home │ home.html └─user user.html / <!– template.html 内容–> <!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>{{title}}</title> </head> <body> <!– njk模板继承后填充 –> {% block content %} {% endblock %} </body> </html> / src/js/home/home.js 一个入口文件/ import ‘../../less/home/home.less’ //引入css import img from ‘../../images/ww.jpg’ //引入图片 console.log(111); let add = (a, b) => a + b; //箭头函数 let a = 3, b = 4; let c = add(a, b); console.log(c); // 这里只做打包演示代码 不具任何意义 <!– less/home/home.less 内容 –> // 引入公共样式 @import ‘../common/common.less’; @import ‘../common/nav.less’; .list { li { color: rebeccapurple; } } .bg-img { width: 200px; height: 200px; background: url(../../images/ww.jpg); // 背景图片 margin: 10px 0; } / webpack配置 webpack.config.js */ const path = require(‘path’); const CleanWebpackPlugin = require(‘clean-webpack-plugin’); const HtmlWebpackPlugin = require(‘html-webpack-plugin’); const MiniCssExtractPlugin = require(“mini-css-extract-plugin”); const CopyWebpackPlugin = require(‘copy-webpack-plugin’); // 多入口 let entry = { home: ‘src/js/home/home.js’, user: ‘src/js/user/user.js’ } module.exports = evn => ({ mode: evn.production ? ‘production’ : ‘development’, // 给每个入口 path.reslove entry: Object.keys(entry).reduce((obj, item) => (obj[item] = path.resolve(entry[item])) && obj, {}), output: { publicPath: ‘/’, filename: ‘js/[name].js’, path: path.resolve(‘dist’) }, module: { rules: [ { // bable 根据需要转换到对应版本 test: /.js$/, exclude: /node_modules/, use: { loader: ‘babel-loader’, options: { presets: [’@babel/preset-env’] } } }, { // 转换less 并交给MiniCssExtractPlug插件提取到单独文件 test: /.less$/, loader: [MiniCssExtractPlugin.loader, ‘css-loader’, ’less-loader’], exclude: /node_modules/ }, { //将css、js引入的图片目录指到dist目录下的images 保持与页面引入的一致 test: /.(png|svg|jpg|gif)$/, use: [{ loader: ‘file-loader’, options: { name: ‘[name].[ext]’, outputPath: ‘./images’, } }] }, { test: /.(woff|woff2|eot|ttf|otf)$/, use: [{ loader: ‘file-loader’, options: { name: ‘[name].[ext]’, outputPath: ‘./font’, } }] } ] }, plugins: [ // 删除上一次打包目录(一般来说删除自己输出过的目录 ) new CleanWebpackPlugin([‘dist’, ‘views/_layout’], { // 当配置文件与package.json不再同一目录时候需要指定根目录 root: path.resolve() }), new MiniCssExtractPlugin({ filename: “css/[name].css”, chunkFilename: “[id].css” }), // 将src下的图片资源平移到dist目录 new CopyWebpackPlugin( [{ from: path.resolve(‘src/images’), to: path.resolve(‘dist/images’) } ]), // HtmlWebpackPlugin 每个入口生成一个html 并引入对应打包生产好的js …Object.keys(entry).map(item => new HtmlWebpackPlugin({ // 模块名对应入口名称 chunks: [item], // 输入目录 (可自行定义 这边输入到views下面的_layout) filename: path.resolve(‘views/_layout/’ + entry[item].split(’/’).slice(-2).join(’/’).replace(‘js’, ‘html’)), // 基准模板 template: path.resolve(‘src/template.html’) })) ] }); <!– package.json中添加 –> “scripts”: { “start”: “node bin/www.js”, “build”: “webpack –env.production –config config/webpack.config.js” } 运行 npm run build 后生成 dist views/_layout 两个目录 <!– 查看打包后生成的模板 views/_layout/home/home.html–> <!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>{{title}}</title> <!– 引入了css文件 –> <link href="/css/home.css" rel=“stylesheet”></head> <body> {% block content %} {% endblock %} <!– 引入了js文件 此时打包后的js/css在dist目录下面 –> <script type=“text/javascript” src="/js/home.js"></script></body> </html> <!– view/home/home.html 页面改写 –> <!– njk继承模板 继承的目标来自webpack打包生成 –> {% extends “../_layout/home/home.html” %} {% block content %} <!– njk引入公共模块 –> {% include “../_component/nav.html” %} <h1>待办事项</h1> <ul class=“list”> <!– 过滤器的调用 timeFormate即我们在中间件中给njk加的过滤器 –> {% for item in todoList %} <li>{{item.name}} —> {{item.time | timeFormate}}</li> {% endfor %} </ul> <div class=“bg-img”> 背景图</div> <!– 页面图片引入方式 –> <img src="/images/ww.jpg"/> {% endblock %} /**koa处理静态资源 * 依赖 npm i ‘koa-static */ // www.js 增加 将静态资源目录指向 打包后的dist目录 app.use(require(‘koa-static’)(path.resolve(‘dist’)))运行npm run buildnpm start浏览器访问127.0.0.1:3000 查看页面 js css img 效果第六步 监听编译目标: 文件发生改实时编译打包依赖 npm i pm2 concurrently /**项目中文件发生变动 需要重启服务才能看到效果是一件蛋疼的事,故需要实时监听变动 */ <!– 我们要监听的有两点 一是node服务 而是webpack打包 package.json变动如下 –> “scripts”: { // concurrently 监听同时监听两条命令 “start”: “concurrently "npm run build:dev" "npm run server:dev"”, “dev”: “npm start”, // 生产环境 执行两条命令即可 无监听 “product”: “npm run build:pro && npm run server:pro”, // pm2 –watch参数监听服务的代码变更 “server:dev”: “pm2 start bin/www.js –watch”, // 生产不需要用监听 “server:pro”: “pm2 start bin/www.js”, // webpack –watch 对打包文件监听 “build:dev”: “webpack –watch –env.production –config config/webpack.config.js”, “build:pro”: “webpack –env.production –config config/webpack.config.js” }第七步 数据请求目标: node请求接口数据 填充模板依赖 npm i node-fetch /*上面的代码中routers/home.js首页路由中我们向页面渲染了下面的一组数据 / ctx.state.todoList = [ {name: ‘吃饭’, time: ‘2019.1.4 12:00’}, {name: ‘下午茶’, time: ‘2019.1.4 15:10’}, {name: ‘下班1’, time: ‘2019.1.4 18:30’} ] /但 数据是同步的 项目中我们必然会向java获取其他后台拿到渲染数据再填充页面 我们来看看怎么做/ /我们在根目录下创建一个util的目录作为工具库 并简单封装fetch.js请求数据/ const nodeFetch = require(’node-fetch’) module.exports = ({url, method, data = {}}) => { // get请求 将参数拼到url url = method === ‘get’ || !method ? “?” + Object.keys(data).map(item => ${item}=${data[item]}).join(’&’) : url; return nodeFetch(url, { method: method || ‘get’, body: JSON.stringify(data), headers: { ‘Content-Type’: ‘application/json’ }, }).then(res => res.json()) } /在根目录下创建一个service的目录作为数据层 并创建一个exampleService.js 作为示例/ //引入封装的 请求工具 const fetch = require(’../util/fetch.js’) module.exports = { getTodoList (params = {}) { return fetch({ url: ‘https://www.easy-mock.com/mock/5c35a2a2ce7b4303bd93fbda/example/todolist', method: ‘post’, data: params }) }, //… } / 将请求加入到路由中 routers/home.js 改写 / const homeRouter = require(‘koa-router’)() let exampleService = require(’../service/exampleService.js’) // 引入service api //将路由匹配回调 改成async函数 并在请时候 await数据回来 再调用render homeRouter.get([’/’, ‘/index.html’, ‘/index’, ‘/home.html’, ‘/home’], async (ctx, next) => { // 请求数据 let todoList = await exampleService.getTodoList({name: ‘ott’}) // 替换原来的静态数据 ctx.state.todoList = todoList.data ctx.render(‘home/home’, { title: ‘首页’ }) }) // 导出路由备用 module.exports = homeRouter第八步 日志打印目标: 使程序运行可视依赖 npm i log4js / 在util目录下创建 logger.js 代码如下 作简单的logger封装 / const log4js = require(’log4js’); const path = require(‘path’) // 定义log config log4js.configure({ appenders: { // 定义两个输出源 info: { type: ‘file’, filename: path.resolve(’log/info.log’) }, error: { type: ‘file’, filename: path.resolve(’log/error.log’) } }, categories: { // 为info/warn/debug 类型log调用info输出源 error/fatal 调用error输出源 default: { appenders: [‘info’], level: ‘info’ }, info: { appenders: [‘info’], level: ‘info’ }, warn: { appenders: [‘info’], level: ‘warn’ }, debug: { appenders: [‘info’], level: ‘debug’ }, error: { appenders: [’error’], level: ’error’ }, fatal: { appenders: [’error’], level: ‘fatal’ }, } }); // 导出5种类型的 logger module.exports = { debug: (…params) => log4js.getLogger(‘debug’).debug(…params), info: (…params) => log4js.getLogger(‘info’).info(…params), warn: (…params) => log4js.getLogger(‘warn’).warn(…params), error: (…params) => log4js.getLogger(’error’).error(…params), fatal: (…params) => log4js.getLogger(‘fatal’).fatal(…params), } / 在fetch.js中是哟logger */ const nodeFetch = require(’node-fetch’) const logger = require(’./logger.js’) module.exports = ({url, method, data = {}}) => { // 加入请求日志 logger.info(‘请求url:’, url , method||‘get’, JSON.stringify(data)) // get请求 将参数拼到url url = method === ‘get’ || !method ? “?” + Object.keys(data).map(item => ${item}=${data[item]}).join(’&’) : url; return nodeFetch(url, { method: method || ‘get’, body: JSON.stringify(data), headers: { ‘Content-Type’: ‘application/json’ }, }).then(res => res.json()) } <!– 日志打印 –> [2019-01-09T17:34:11.404] [INFO] info - 请求url: https://www.easy-mock.com/mock/5c35a2a2ce7b4303bd93fbda/example/todolist post {“name”:“ott”}注: 仅共学习参考,生产配置自行斟酌!转载请备注来源! ...

January 9, 2019 · 8 min · jiezi

从搭建脚手架到在npm上发布react组件

从搭建脚手架到在npm上发布react组件最近公司给公司里架设了私有的npm仓库,相应地也需要一个用来发布react组件用的脚手架,在这个过程中又又又又复习了一下webpack,在这里分享下脚手架搭建的过程。首先,我们预期的脚手架具有如下功能开发组件时可以实时预览对组件各种资源进行打包(js/css/图片等)一键打包发布1.创建项目脚手架的名字暂时取react-simple-component-boilerplate。首先创建一个新目录用于放我们的文件:mkdir react-simple-component-boilerplatecd react-simple-component-boilerplate使用npm命令创建一个项目npm init接下来会提示你输入项目的名称、版本号、作者等,也可以一路回车,稍后修改。这一步完成后,你的项目文件夹里应该有一个package.json文件了,这个文件保存了我们项目和组件的各种信息。接下来创建如下的目录结构react-simple-component-boilerplate |– config // webpack配置 |– demo // 开发时预览用 |– dist // 打包结果 |– src // 源文件目录 | – assets // 存放图片等媒体文件 | – style // 存放样式,项目使用的是less来编写样式2.安装依赖既然我们要发布的是react组件,那依赖里肯定少不了react。使用npm install安装下面的依赖npm install react react-dom –save打包工具选择的是webpack,下面是开发依赖,也需要一并安装 “devDependencies”: { // babel用于将你写的es6+的代码转换到es5 “@babel/cli”: “^7.0.0”, “@babel/core”: “^7.0.0”, “@babel/plugin-proposal-class-properties”: “^7.0.0”, // 用于支持class属性 “@babel/plugin-proposal-decorators”: “^7.0.0”, // 支持decorator “@babel/plugin-transform-modules-commonjs”: “^7.0.0”, “@babel/plugin-transform-runtime”: “^7.0.0”, // 自动polyfill es5不支持的api特性 “@babel/preset-env”: “^7.0.0”, // 根据目标环境来按需转码 “@babel/preset-react”: “^7.0.0”, // 让babel支持react语法 “babel-loader”: “^8.0.0”, “css-loader”: “^1.0.0”, “file-loader”: “^2.0.0”, “html-loader”: “^0.4.4”, “less-loader”: “^4.1.0”, // 使用less来编写样式 “mini-css-extract-plugin”: “^0.5.0”, // 将css提取成一个单独的文件 “style-loader”: “^0.23.0”, “webpack”: “^4.26.0”, “webpack-cli”: “^3.1.2”, // webpack4之后需要额外安装webpack-cli “webpack-dev-server”: “^3.1.14”, // 开发时预览组件所用的服务,在文件变化时会自动刷新页面 “webpack-merge”: “^4.1.4” // 用于合并webpack配置 },3.编写组件在/src目录下新建一个index.js,这就是我们组件的入口文件了。如果项目中要使用图片、css等,分类放到assets、style文件夹下就好。下面我们就在index.js中写一个简单的组件/* src/index.js */import React from ‘react’;import ‘./style/style.less’; // 使用less的情况import testPng from ‘./assets/test.png’; // 使用图片的情况export default class MyComponent extends Component { render(){ return (<div>A new Component</div>) }}接下来,我们在/demo目录下新建index.html和demo.js这两个文件用于在开发组件时预览组件效果。index.html内容如下<!DOCTYPE html><html><head> <meta charset=“UTF-8”> <title>Title</title></head><body><div id=“root”></div><script src=“demo.bundle.js”></script></body></html>在demo.js中,我们要使用一下刚刚写的组件(位于/src/index.js)看一下效果,开发中这个demo.js文件会被打包成demo.bundle.js,就是在上面index.html中引用的js。import React from ‘react’;import ReactDom from ‘react-dom’;import MyComponent from ‘../src/index’const Demo = () => { return <div> <h1>组件预览:</h1> <MyComponent /> </div>}ReactDom.render(<Demo />, document.getElementById(‘root’));4.配置webpack和babel4.1 配置webpack在/config下我们建立三个webpack配置文件webpack.base.jswebpack.config.dev.js // 开发时的配置webpack.config.prod.js // 打包发布时的配置由于开发和发布打包时webpack的配置有一部分是公共而且重复的,我们把这部分的配置单独拿出来放到webpack.base.js中。首先是公共配置webpack.base.js:module.exports = { module: { rules: [ { // 在webpack中使用babel需要babel-loader test: /.js?$/, loader: ‘babel-loader’, exclude: ‘/node_modules/’, }, { // 用于加载组件或者css中使用的图片 test: /.(jpg|jpeg|png|gif|cur|ico|svg)$/, use: [{ loader: ‘file-loader’, options: { name: “images/[name][hash:8].[ext]” } }] } ] }}下面是开发时所用的webpack配置,写在webpack.config.dev.js中const path = require(‘path’);const merge = require(‘webpack-merge’);const baseConfig = require(’./webpack.base.js’); // 引用公共的配置const devConfig = { entry: ‘./demo/demo.js’, // 入口文件 mode: ‘development’, // 打包为开发模式 output: { filename: ‘demo.bundle.js’, // 输出的文件名称 path: path.resolve(__dirname, ‘../demo’) // 输出的文件目录 }, devServer: { // 该字段用于配置webpack-dev-server contentBase: path.join(__dirname, ‘../demo’), compress: true, port: 9000, // 端口9000 open: true // 自动打开浏览器 }, module: { rules: [ { // 编译less test: /.less$/, exclude: ‘/node_modules/’, use: [{ loader: ‘style-loader’ }, { loader: ‘css-loader’ }, { loader: ’less-loader’ }] }, ] },}module.exports = merge(devConfig, baseConfig); // 将baseConfig和devConfig合并为一个配置需要注意的是,等会使用webpack-dev-sevrer启动开发服务时,并不会实际在demo文件夹下生成demo.bundle.js,打包好的文件是在内存中的,但并不影响我们使用。下面是打包发布时所用的webpack配置,写在webpack.config.prod.js中const path = require(‘path’);const merge = require(‘webpack-merge’);const baseConfig = require(’./webpack.base.js’);const MiniCssExtractPlugin = require(“mini-css-extract-plugin”); // 用于将组件的css打包成单独的文件输出到dist目录中const devConfig = { entry: ‘./src/index.js’, mode: ‘production’, output: { path: path.resolve(__dirname, ‘../dist’), filename: ‘index.js’, // 输出文件 libraryTarget: ‘umd’, // 采用通用模块定义, 注意webpack到4.0为止依然不提供输出es module的方法,所以输出的结果必须使用npm安装到node_modules里再用,不然会报错 library: ‘react-simple-component-boilerplate’, // 库名称 libraryExport: ‘default’, // 兼容 ES6(ES2015) 的模块系统、CommonJS 和 AMD 模块规范 }, externals: { react: { root: “React”, commonjs2: “react”, commonjs: “react”, amd: “react” }, “react-dom”: { root: “ReactDOM”, commonjs2: “react-dom”, commonjs: “react-dom”, amd: “react-dom” } }, module: { rules: [{ test: /.(le|c)ss$/, use: [ MiniCssExtractPlugin.loader, “css-loader”, { loader: “less-loader”, options: { sourceMap: false } } ] } ] }, plugins: [ new MiniCssExtractPlugin({ filename: “main.min.css” // 提取后的css的文件名 }) ],}module.exports = merge(devConfig, baseConfig);上面我们配置了externals字段,这一点非常重要。externals定义了外部依赖。将react和react-dom添加进该字段,说明我们的组件将依赖外部的react和react-dom,这样就可以避免把react和react-dom打包进去(不然组件会很大)4.1 配置babel我们需要用babel把我们的代码编译成es5版本。在项目根目录新建一个.babelrc文件,输入以下内容。{ “presets”: [ [ “@babel/preset-env”, { “targets”: “> 0.25%, not dead” } ], “@babel/preset-react” ], “plugins”: [ “@babel/plugin-transform-runtime”, “@babel/plugin-transform-modules-commonjs”, [ “@babel/plugin-proposal-decorators”, { “legacy”: true } ], “@babel/plugin-proposal-class-properties”, “@babel/plugin-proposal-object-rest-spread” ]}我们在presets其中使用了preset-env, 规定了输出的代码目标环境是份额大于0.25%的浏览器。另外由于我们的项目里使用了react,presets中就要加入preset-react。同时,plugins配置了一些babel插件,用于支持装饰器展开操作符等类内直接定义属性等新的es特性。4.3 配置启动命令我们再次回到项目根目录下的package.json中,编辑如下 “scripts”: { “build”: “set NODE_ENV=production && webpack –config ./config/webpack.config.prod.js”, “pub”: “npm run build && npm publish”, “dev”: “webpack-dev-server –config ./config/webpack.config.dev.js” }, “main”: “dist/index.js”, “files”: [“dist”]build 命令用于打包组件dev 命令会使用webpack-dev-server启动一个开发服务用于预览组件效果pub 命令进行打包组件并且发布到npm上main字段指定了我们的组件的入口文件,files字段用于指定我们的npm包的文件目录。5.试用和发布要发布一个npm包,我们需使用如下命令添加一个npm的账号,如果已经添加过的这一步可以跳过。npm adduser如果已经有npm账号,可以使用npm login登陆。如果不知道自己是否已经添加过了npm账号,使用npm whoami查看登陆信息即可接下来就编辑package.json把组件的名称/版本/介绍等字段都填写一下。好了,接下我们先使用npm run dev命令,此时会自动打开默认浏览器预览组件。如果没什么问题的话,接下来使用npm run pub进行打包和发布。等待发布完成后,我们就下载安装一下。npm i your-component // 假设你的包名字叫your-component使用自己发布的组件import YourComponent from ‘your-component’;import ‘your-component/dist/main.min.css’; // 如果给组件写了样式,需要手动导入css文件6.总结到这里,一个非常非常简单的用于发布react小组件的脚手架就搭好了,总结一下其中要注意的地方:webpack打包时libraryTarget要使用umdexternals 里要把外部依赖配置好如果还要生成es module,可以额外使用gulp或rollup等工具webpack4 之后建议使用MiniCssExtractPlugin来提取css ...

January 9, 2019 · 3 min · jiezi

关于webpack的完整学习

终于学习了一下webpackwebpack 是前端的一个项目构建工具,它是基于Node.js开发出来的一个前端工具。WebPack可以看做是模块打包机(bundler),通过分析项目结构,找到JavaScript模块以及其它的一些浏览器不能直接运行的拓展语言(Scss,TypeScript等),并将其打包为合适的格式以供浏览器使用。为什么要使用webpackJavaScript和CSS的依赖问题。开发过程中,JavaScript和CSS的在页面中的顺序问题,经常会造成CSS没起作用,JavaScript的某个变量和方法找不到。性能优化。一般浏览器请求的文件越多越耗时,请求的文件越大越耗时,尤其是为了前端项目的代码更清晰,结构更合理,我们采用了MVC,MVVM等很多架构分解出了很多JS文件,无疑又拖慢了网页的速度。为了解决这个问题,一般会采用以下两个方案:(1)、 文件合并。浏览器需要下载多个JS文件,而浏览器是有并发限制,也就是同时并发只能下载几个文件。当需要加载的文件非常多,网页的性能可想而知,所以我们需要合并多个文件以减少文件的数量。(2)、 文件压缩。我们知道文件越大,下载越慢,而针对JavaScript和CSS,里面的空格,换行这些都是为了让我们读代码时更容易阅读,但是对机器来说,这些对它没有影响。提高开发效率。主要体现在:(1)、 Vendor前缀。在CSS3使用越来越多的时候,我们都知道一些CSS3的新特性,在不同的浏览器中,CSS有不同的前缀,如果我们手工添加将会很繁琐,而如果使用构建工具,很多构建工具可以自动给我添加CSS的Vendor前缀。关于vendor前缀的学习(2)、单元测试。JavaScript的单元测试在使用MVC或者MVVM的框架后,变得越来越容易,而单元测试是质量保证的一个很重要的手段,所以在提交之前,使用构建工具自动跑一遍我们的单元测试是非常有必要的,能进一步检测你的项目的健壮性和容错能力。关于MVVM的学习(3)、代码分析。我们写的JavaScript很多时候会有一些潜在的bug, 比如忘了添加分号,某个变量没有等等,使用一些JavaScript的代码分析工具,可以很好的帮我们检查一些常见的问题。(4)、版本升级。Grunt和GulpGulp/Grunt是基于task任务的一种能够优化前端的开发流程的工具,而WebPack是一种模块化的解决方案,是基于整个项目进行构建的。Gulp/Grunt在一个配置文件中,指明对某些文件进行类似编译,组合,压缩等任务的具体步骤,工具之后可以自动替你完成这些任务。如图:而webpack把你的项目当做一个整体,通过一个给定的主文件(如:index.js),Webpack将从这个文件开始找到你的项目的所有依赖文件,使用loaders处理它们,最后打包为一个(或多个)浏览器可识别的JavaScript文件。如图:Webpack的优点使得Webpack在很多场景下可以替代Gulp/Grunt类的工具。loaderwebpack本身只能加载js/json模块,如果要加载其它类型的文件/模块, 就需要使用对应的loader进行转化和加载。loader可以将所有类型的文件转换为webpack 能够处理的有效模块,然后你就可以利用 webpack 的打包能力,对它们进行处理。loader一般以 xxx-loader 的方式命名,xxx代表了这个loader要做的转化功能,比如:css-loader,json-loader等。webpack安装全局安装:npm i webpack -g项目依赖:npm i webpack –save-dev简单示例webpack01.html<body> <div id=“test”> 哈哈哈 </div> <script type=“text/javascript” src=“js/test.js”></script></body>test.jsimport $ from ‘jquery’;import ‘../css/common.css’;$(function () { $(’#test’).addClass(“test”);});通过命令打包:webpack js/test.js –output dist/bundle.js –mode development打包完成后项目下多出dist文件夹,把webpack01.html拷贝进去,在页面上访问,可以出现相同效果webpack把所有依赖打包成一个bundle.js文件,通过代码分割成单元片段并按需加载。关于webpack打包原理学习于:https://blog.csdn.net/baozhiq…webpack配置文件除了以上通过命令来打包,还可以通过配置文件配置webpack的入口文件等等信息,在终端直接执行webpack即可打包。webpack.config.jsconst path = require(‘path’);module.exports = { mode: ‘production’, // 入口文件配置 entry: ‘./js/test.js’, // 输出配置 output: { // 出口文件 filename: ‘bundle.js’, // 设置全路径 path: path.resolve(__dirname, ‘dist’) }, module: { // 规则数组, 里面的每一个对象都是在描述一个loader rules: [ { //css文件加载,正则表达式css文件的路径 test: /.css$/, use: [ ‘style-loader’, ‘css-loader’ ] }, { ////图片文件加载 test: /.(png|jpg|gif)$/, use: [ { loader: ‘file-loader’, query: { name: ‘img/[name]-[hash:5].[ext]’ } }, ], }, ] }, devServer:{ contentBase: ‘./dist’ }};关于配置文件详解来自于:https://blog.csdn.net/qq_3977…css文件打包webpack打包css文件需要添加style-loader和css-loader模块:npm install css-loader style-loader –save-dev(或者通过配置文件引入)css-loader让我们能使用类似@import和url(…)的方法实现require()的功能,style-loader将所有的计算后的样式加入页面中,二者组合在一起能够把样式表嵌入webpack打包后的JS文件中。关于测试代码,上面示例已展示。webpack插件插件(Plugins)是用来拓展Webpack功能的,它们会在整个构建过程中生效,执行相关的任务。HtmlWebpackPlugin该插件依据一个简单的模板,帮你生成最终的Html5文件,这个文件中自动引用了你打包后的JS文件。通过`npm install –save-dev html-webpack-plugin命令安装。webpack.config.js的配置:使用:删除掉原先打包编译生成的dist文件夹,重新执行webpack,该插件会自动根据指定的模板页面帮助我们创建index.html文件,并做好相关配置引入。自动编译打包webpack-dev-server提供了一个简单的web服务器,并且能够实时重新加载(live reloading)。通过npm install –save-dev webpack-dev-server命令安装。webpack.config.js的配置:编译打包:package.json中"scripts": { “test”: “echo "Error: no test specified" && exit 1”, “start”: “webpack-dev-server –open”, “build”: “babel js –out-dir lib” }终端执行npm start即可。 ...

January 5, 2019 · 1 min · jiezi

vue-router和webpack懒加载,页面性能优化篇

在vue单页应用中,当项目不断完善丰富时,即使使用webpack打包,文件依然是非常大的,影响页面的加载。如果我们能把不同路由对应的组件分割成不同的代码块,当路由被访问时才加载对应的组件(也就是按需加载),这样就更加高效了。——引自vue-router官方文档如何实现??vue异步组件vue-router配置路由,使用vue的异步组件技术,可以实现懒加载,代码如下:// 每个组件都会生成一个js文件import Vue from ‘vue’import Router from ‘vue-router’import Login from ‘../view/List.vue’;Vue.use(Router);export default new Router({routes: [ { path: ‘/home/list’, name: ’list’, components: resolve => require([’../view/List.vue’], resolve) }],动态import(webpack > 2.4)vue、webpack官方推荐情况一:每个组件都会打包生成一个js文件const List = () => import(’../view/List.vue’)// 在路由配置中什么都不需要改变,像往常一样使用组件:export default new Router({routes: [ { path: ‘/home/list’, name: ’login’, components: Login }, { path: ‘/home/user’, name: ‘user’, components: User }],情况二:所有组件合并打包在一个异步块chunk中const List = () => import(/* webpackChunkName: “home” / ‘./List.vue’)const User = () => import(/ webpackChunkName: “home” / ‘./User.vue’)// 在路由配置中什么都不需要改变,像往常一样使用组件:export default new Router({routes: [ { path: ‘/home/list’, name: ’list’, components: List }, { path: ‘/home/user’, name: ‘user’, components: User }],// 在webpack.base.config.js中配置 ChunkFileName:output: {path: config.build.assetsRoot,filename: ‘[name].js’,chunkFilename: ‘[name].js’,publicPath: process.env.NODE_ENV === ‘production’ ? config.build.assetsPublicPath : config.dev.assetsPublicPath},另一种写法,更简洁:同样需要在webpack.base.config.js中配置 ChunkFileName:{ path: ‘/home/list’, name: ’list’, component: () => import(/ webpackChunkName:“list”/ ‘../view/List.vue’)},{ path: ‘/home/user’, name: ‘user’, component: () => import(/ webpackChunkName:“user”*/ ‘../view/User.vue’)},webpack提供的require.ensure()语法如下:摘自官网require.ensure(dependencies: String[], callback: function(require), chunkName: String多个路由指定相同的chunkName,在这里chunkName为home,会合并打包成一个js文件。{ path: ‘/home/list’, name: ’list’, // component:list component: r => require.ensure([], () => r(require(’../view/Lst.vue’)), ‘home’)},{ path: ‘/home/user’, name: ‘user’, // component:user component: r => require.ensure([], () => r(require(’../view/User.vue’)), ‘home’)}// 在webpack.base.config.js中配置 ChunkFileName 和 publicPath:output: {path: config.build.assetsRoot,filename: ‘[name].js’,chunkFilename: ‘[name].js’,publicPath: ‘./’,publicPath: process.env.NODE_ENV === ‘production’ ? config.build.assetsPublicPath : config.dev.assetsPublicPath},在实践过程中应该会遇到各种问题,到时候再继续补充,前端新手,多多指教! ...

January 5, 2019 · 1 min · jiezi

webpack url-loader limit 转换部分资源为base64格式 其余不转换

在Vue-cli中,我们默认使用webpack将所有的小于限制数值的资源转为base64格式:如下:{ test: /.(png|jpe?g|gif|svg)(?.)?$/, loader: ‘url-loader’, options: { limit: 10, } }, /limit: 10,限制 图片大小 10B,小于限制会将图片转换为 base64格式/ 这样做的好处是:将资源转化为base64可以尽可能的减少网络请求次数、提前加载图片(网络不好时候提前显示图片),但是也有很大的缺点,这也是这篇文章主要想表达的问题。使用base64的缺点大概有:1、若转化的图片太大,导致数据太大,加载过慢,所以一般像Vue在limit通常默认设置的是10000B。2、使用base64的资源将不会拥有缓存,即如果些张图片被用来做为有频率切换动画(例如自定义模拟喇叭播放的动画)这样就会由于图片没有缓存而反复请求导致其他的资源请求排队,pending时间从几秒到一分钟不等的问题。3、在微信小程序里,我们知道背景图片是只能使用base64格式的,但若是其他的静态资源不想使用base64又该如何呢?下面这种情况是对于不同格式的图片的定义方法,可以根据不同格式的图片做自定义配置://webpack.js 中loader配置{ test: /.(gif|jpg)$/, loader: ‘url-loader?limit=10000’, options: { name: ‘[name].[ext]?[hash]’ }},{ test: /.(png|svg)$/,//(png|jpg|gif|svg) loader: ‘url-loader’, options: { limit: 10, name: utils.assetsPath(‘img/[name].[hash:7].[ext]’) }}假如我们想使用一部分base64格式的图片(音频、视频),做法如下:/webpack.base.conf.js/module: { rules: [ { test: /.(png|jpe?g|gif|svg)(?.)?$/, loader: ‘url-loader’, options: { limit: 10, name: ‘static/images/[name].[ext]’, } } ]} /* page/index.vue /<script> let { image_k1 } = { // !注释很重要 / eslint import/no-webpack-loader-syntax: off */ image_k1: require(’!url-loader?limit=10000!../../static/images/image_k1.png’) } </script><template> <div :style="{backgroundImage:url(${image_k1})}"></div></template>这样就完成了某些资源是base64,其他资源不变的效果。当然 你也可以选择直接在线转换后放到css里然后引用,效果一样。 ...

January 3, 2019 · 1 min · jiezi

Webpack Bundle Analyzer插件的使用

一、原理读取输出文件夹(通常是dist)中的stats.json文件,把该文件可视化展现的插件。便于直观地比较各个bundle文件的大小,以达到优化性能的目的。二、安装> npm install webpack-bundle-analyzer –save-dev三、作为webpack插件使用(1)引入const BundleAnalyzerPlugin = require(‘webpack-bundle-analyzer’).BundleAnalyzerPlugin(2)使用webpackConfig.plugins.push(new BundleAnalyzerPlugin({…}))四、2种方式(1)每次构建时自动打开构建完成之后,浏览器会自动打开localhost:8888,不用改动package.jsonwebpackConfig.plugins.push(new BundleAnalyzerPlugin({ analyzerMode: ‘server’, generateStatsFile: true, statsOptions: { source: false }}))配置参数记得补上,不然构建完不会自动打开~(2)运行特定命令才打开把analyzerMode设置为disabledwebpackConfig.plugins.push(new BundleAnalyzerPlugin({ analyzerMode: ‘disabled’, generateStatsFile: true, statsOptions: { source: false }}))在package.json的scripts字段中加入"bundle-report": “webpack-bundle-analyzer –port 8123 dist/stats.json"其中stat.json文件的位置在打包后的文件夹中,通常是dist,具体情况根据实际情况来定。命令行中键入命令npm run bundle-report浏览器自动打开分析页面:localhost:8123

January 2, 2019 · 1 min · jiezi

webpack打包JS

目录结构使用在根目录下输入yarn,可以看到生成node_modules目录。在根目录下输入yarn run build,可以看到生成dist/bundle.js,这个就是打包输出的内容。在根目录下输入yarn run dev,在浏览器输入http://localhost:8080/,可以看到控制台输出结果3.文件解析package.json{ “scripts”: { “dev”: “webpack-dev-server –mode development webpack.config.js”, “build”: “webpack –mode production –config webpack.config.js” }, “devDependencies”: { “webpack”: “^4.16.1”, “webpack-cli”: “^3.1.0”, “webpack-dev-server”: “^3.1.4” }}scripts下的是命令行可以输入的指令,比如yarn run dev执行的就是第一条;devDependencies是命令行输入yarn时安装的包。index.html<html> <head>Test Webpack Js</head> <meta charset=“UTF-8”> <body> <script src="./dist/bundle.js"></script> </body></html>可以看到引用的是打包后的js文件,bundle.jsapp.js// es6import sum from “./vendor/sum”;console.log(“sum(1, 2) = “, sum(1, 2));// commonJsvar sum1 = require(”./vendor/sum1”);console.log(“sum1(2, 3) = “, sum1(2, 3));package.json中两条命令打包的入口就是app.js,他引用的是要合并的两个js文件vendor/sum.js// es6export default function (a, b) { return a + b;}vendor/sum1.js// commonJsmodule.exports = function(a, b) { return a + b;};参考链接 ...

January 2, 2019 · 1 min · jiezi

webpack tree shaking 总结

原文链接 https://www.webpackjs.com/gui…什么是tree shakingtree shaking 是一个术语,用于描述移除JavaScript 上下文中的未引用代码为什么可以实现它依赖ES2015 模块系统中的静态结构特性,例如import 和export在webpack 中如何用版本要求:webpack 4。 在package.json 中添加 sideEffects.副作用的定义是,在导入时会执行特殊行为的代码,而不是仅仅暴露一个export 或多个export。例如 polyfill, 它影响全局作用域,并且通常不提供export。如果所有代码都不包含副作用,将其设置为false. webpack 就可以安全地删除未用到的export 导出如果你的代码确实有一些副作用,你可以为sideEffects 提供一个数组。ps: 任何导入的文件都会受到tree shaking 的影响。这意味着,如果在项目中使用类似css-loader 并导入css文件,需要将其添加到side effect 列表中,以免在生成模式中无意将它删除。压缩输出设置webpack mode: production 就可以在bundle 中删除那些未被引用的代码2019年第一篇

January 2, 2019 · 1 min · jiezi

@Vue/Cli 3 Invalid Host header 错误解决办法

我的host设置报错如下:解决办法:@Vue/Cli 3 在vue.config.js里加上.disableHostCheck(true)vue-cli 在webpack.dev.conf.js里加上devServer: { disableHostCheck: true}最后

January 2, 2019 · 1 min · jiezi

webpack学习进阶(三)

不知不觉,webpack文章写道第三篇了,上篇文章写了webpack的配置分离,这里讲一下webpack的代码分离,webpack chunk和一些webpack的调试吧。代码分离代码分割不仅仅是提取业务的公共代码,更应该关注的是实现代码的按需加载。 通过在模块中定义 split point ,webpack 会在打包的时候自动的分割代码块。定义 split point 的方式有两种:CommonJs 方式require.ensure 方法:/** * @param dependencies [Array] 模块依赖的数组 * @param callback [Function] /require.ensure(dependencies, callback)eg:// 模块 index.js/* * [description] * @param {[type]} ) { const async [description] * @return {[type]} [description] /require.ensure([’./async’], function() { const async = require(’./async’); console.log(async.default)});// 模块 async.jsexport default { a: 1}webpack 打包过后会生成三个文件index.js 1.1.js vendor.common.jsindex.js 的内容为:webpackJsonp([0],[/ 0 /// function(module, exports, webpack_require) { ‘use strict’; webpack_require.e/ nsure /(1, function () { var async = webpack_require(1); console.log(async.default); });// }]);//# sourceMappingURL=home.js.map1.1.js 的内容为webpackJsonp([1],[/ 0 /,/ 1 /// function(module, exports) { “use strict”; Object.defineProperty(exports, “__esModule”, { value: true }); / * async module / exports.default = { a: 1 };// }]);//# sourceMappingURL=1.1.js.mapwebpackJsonp 方法定义在 vendor.bundle.js 中:window[“webpackJsonp”] = function webpackJsonpCallback(chunkIds, moreModules) {// // add “moreModules” to the modules object,// // then flag all “chunkIds” as loaded and fire callback// var moduleId, chunkId, i = 0, callbacks = [];// for(;i < chunkIds.length; i++) {// chunkId = chunkIds[i];// if(installedChunks[chunkId])// callbacks.push.apply(callbacks, installedChunks[chunkId]);// installedChunks[chunkId] = 0;// }// for(moduleId in moreModules) {// modules[moduleId] = moreModules[moduleId];// }// if(parentJsonpFunction) parentJsonpFunction(chunkIds, moreModules);// while(callbacks.length)// callbacks.shift().call(null, webpack_require);// if(moreModules[0]) {// installedModules[0] = 0;// return webpack_require(0);// }// };所以在 html 中应该先加载 vendor.bundle.js<script src=“js/vendor.bundle.js”></script><script src=“js/index.js”></script>在使用异步加载的时候需要注意一下两点:配置 output.publicPath: publicPath 为脚本在服务器中的文字,只有定义了 publicPath 才能通过 webpackJsonp 获取;是对于异步的 CommonJs 模块引入只能通过 require 的方式引用,不能通过 Es6 import 的方式引入;AMD 方式AMD 的方式也可以实现异步加载,这和使用 require.js 的使用方式基本相同,在定义模块的时候需要按照 AMD 的规范来定义/ * @param dependencies [Array] 模块依赖 * @param callback [Function] /require(dependencies, callback)eg:// 定义模块define(‘modula-a’, [‘module-c’], function(c) { // return …})// 依赖模块require([“module-a”, “module-b”], function(a, b) { console.log(a, b);});这时候的 require 实现是异步的方式,只有依赖的模块加载完成并执行回调,才会执行模块的 callback,依赖模块的回调结果会作为参数传入 a, b 中。代码块 Chunk什么是Chunk?webpack 中 Chunk 实际上就是输出的 .js 文件,可能包含多个模块,主要的作用是为了优化异步加载。对于同步的情况,一个 Chunk 会递归的把模块中所有的依赖都加入到 Chunk 中。对于异步的情况,在每一个 split point 上所有依赖的模块会打包进一个新的 chunk,和同步一样,依赖也是递归的,如果子模块依赖其他模块也会加入到 chunk 中,依赖的回调函数中 require 的其他模块也会打包进 chunk 中,以下公式表示 chunk 内容:chunk content = recursive(ensure 依赖) + recursive(callback 依赖)Chunk 分类Entry Chunk入口代码块包含了 webpack 运行时需要的一些函数,如 webpackJsonp, webpack_require 等以及依赖的一系列模块。Normal Chunk普通代码块没有包含运行时需要的代码,只包含模块代码,其结构有加载方式决定,如基于 CommonJs 异步的方式可能会包含 webpackJsonp 的调用。 The chunk also contains a list of chunk id that it fulfills.Initial chunk与入口代码块对应的一个概念是入口模块(module 0),如果入口代码块中包含了入口模块 webpack 会立即执行这个模块,否则会等待包含入口模块的代码块,包含入口模块的代码块其实就是 initial chunk。 以上面的 CommonJs 异步加载为例:<!– 入口 Chunk, 未包含入口模块 –><script src=“js/vendor.bundle.js”></script><!– 包含入口模块的 Initial Chunk,执行入口模块 –><script src=“js/index.js”></script>Commons chunk 与 CommonsChunkPlugin之前我们已经利用 CommonsChunkPlugin 来分割公共代码如 react, react-dom 到 vendor.bundle.js 中,这里介绍相关的原理。以下面的配置为例var webpack = require(“webpack”);module.exports = { entry: { a: “./a”, b: “./b” }, output: { filename: “[name].js” }, plugins: [ new webpack.optimize.CommonsChunkPlugin(“init.js”) ]}当有多个入口的时候,CommonsChunkPlugin 会把 a,b 模块公共依赖的模块抽离出来,并加上 webpack 运行时代码,形成一个新的代码块,这个代码块类型为 entry chunk。a,b 两个入口会形成两个单独的代码块,这两个代码块为 initial chunk。在 html 中,可以如下加载:<!– entry chunk –><script src=“init.js”></script><!– inital chunk a –><script src=“a.js”></script><!– initial chunk b –><script src=“b.js”></script>require.include可以使用 require.include 方法,直接引入模块,如下例子:require.ensure(["./file"], function(require) { require("./file2");});// is equal torequire.ensure([], function(require) { require.include("./file"); require("./file2");});这个方法可以实现一些分块的优化,当一个 chunk-parent 可能会异步引用多个 chunk-child 而这些 chunk-child 可能都包含了 moduleA, 那么可以在 chunk-parent 中 require.include(‘moduleA’) 就可以避免重复加载 moduleA。给异步 Chunk 命名根据 split point 生成出来的 chunk 名称都是数字,可以在 split point 上定义 chunk 名称:/* * @param chunkName [String] chunk 名称 */require.ensure(dependencies, callBack, chunkName)也可以在 webpack.config.js 中配置修改 output.chunkName 来修改 chunk 名称Chunk 优化在一些特殊的场景可以利用如下这些插件来完成 Chunk 的优化,LimitChunkCountPluginMinChunkSizePluginAggressiveMergingPluginwebpack调试在配置 webpack 的过程中,可以利用 webpack 提供的一些工具和参数来调试。命令行参数$ webpack [–params,…]1.–progress: 能够看到打包进度2.–json: 可以将打包结果输出为 json3.–display-chunks: 可以看到打包出来的 chunk 信息webpack analyse可以通过 analyse 网站分析 webpack 的编译结果,如下图,可以分析 chunks, modules, Assets 等。 ...

January 1, 2019 · 3 min · jiezi

webpack学习进阶(二)

在上篇文章中,我们了解了一些webpack的基础配置,接下来我们来进一步了解webpack的高级使用。配置分离随着我们业务逻辑的增多,图片、字体、css、ES6以及CSS预处理器和后处理器逐渐的加入到我们的项目中来,进而导致配置文件的增多,使得配置文件书写起来比较繁琐,更严重者(书写特定文件的位置会出现错误)。更由于项目中不同的生产环境和开发环境的配置,使得配置文件变得更加糟糕。使用单个的配置文件会影响到任务的可重用性,随着项目需求的增长,我们必须要找到更有效地管理配置文件的方法。管理配置文件的几种方法:在每个环境的多个文件中维护配置,并通过–config参数将webpack指向每个文件,通过模块导入共享配置。将配置文件推送到库,然后引用库。将配置文件推送到工具。维护单个配置文件的所有配置并在那里进行分支并依赖–env参数。在本文中,主要介绍第四种文件配置的方法。分离配置文件我们在根目录下创建config文件夹,并创建四个配置文件:webpack.comm.js 公共环境的配置文件webpack.development.js 开发环境下的配置文件webpack.production.js 生产环境下的配置文件webpack.parts.js 各个配置零件的配置文件合并配置文件的工具如果配置文件被分成了许多不同的部分,那么必须以某种方式来组合他们,通常就是合并数组和对象,webpack-merge很好的做到了这一点。webpack-merge做了两件事:它允许连接数组并合并对象,而不是覆盖组合。如下所示:const merge = require(“webpack-merge”);merge( {a : [1],b:5,c:20}, {a : [2],b:10, d: 421})//合并后的结果{a : [1,2] ,b :10 , c : 20, d : 421}那么,如何合并?1.首先将webpack-merge添加到项目中npm install webpack-merge –save-dev2.设置各个配置文件的连接webpack.config.jsconst commConfig = require("./config/webpack.comm");const developmentConfig = requie("./config/webpack.development");const productionConfig = require("./config/webpack.development")const merge = require(“webpack-merge”);module.exports = mode => { if(mode === “production”){ return merge(commConfig,productionConfig,{mode}); } return merge(commConfig,developmentConfig,{mode});}注:上面代码利用mode的值来判断是开发环境还是生产环境webpack.comm.jsconst merge = require(“webpack-merge”);const parts = require("./webpack.parts") //引入配置零件文件const config = { //书写公共配置 }module.exports = merge([ config, parts……])webpack.production.jsconst merge = require(“webpack-merge”);const parts = require("./webpack.parts"); //引入配置零件文件const config = { //书写公共配置}modules.exports = merge([ config, parts……])webpack.development.jsconst merge = require(“webpack-merge”);const parts = require("./webpack.parts"); //引入配置零件文件const config = { //书写公共配置}modules.exports = merge([ config, parts……])–env值传参 “dev”: “webpack –env development “, “prod”: “webpack –env production”, “dev:server”: “webpack-dev-server –env development “使用–env允许将字符串传递给配置。我们来修改下package.json,使得env参数mode环境参数传入到webpack.config.js中,就可以判断是生产环境还是开发环境。如何写出可配置的webpack.parts.js上面可以看出我们新建了一个webpack.parts.js文件,这个文件中主要是存放我们的一些配置零件。如何写出可配置,可拔插的配置零件。就是我们这个文件的最重要的部分。以loadCSS为例:exports.loadCSS = ({reg = /.css$/,include,exclude,uses = []} = {}) => ({ module : { rules:[{ test : reg, include, exclude, use[{ loader : “style-loader”, },{ loader : “css-loader”, }].concat(uses), }] }})上面代码中,利用exports导出单个配置零件,通过解构赋值来传入参数。使用数组的concat来连接外部导入的loader。参数解析:reg:表示loader匹配的test正则,默认为css,这里可以是(less、sass、stylus)。include:表示所要打包的文件夹。exclude:表示要跳过打包的文件夹。uses:外部导入的loader。在webpack.development.js中引入module.exports = merge([ config, parts.loadCSS({ reg : /.less/, use : [“less-loader”] }), parts.loadCSS(),])总结:配置文件拆分可以是我们继续扩展配置。最重要的收益是我们可以提取不同目标之间的共性。并且还可以识别要组合的较小配置部件,这些配置不见可以推送到自己的软件包以跨项目使用。还可以将配置作为依赖项进行管理,而不是在多个项目中复制类似的配置。 ...

January 1, 2019 · 1 min · jiezi

webpack学习进阶(一)

首先,webpack是什么?webpack是模块化管理工具,使用webpack可以对模块进行压缩、预处理、按需打包、按需加载等。为什么使用webpack?对 CommonJS 、AMD 、ES6的语法做了兼容;对js、css、图片等资源文件都支持打包;串联式模块加载器以及插件机制,让其具有更好的灵活性和扩展性,例如提供对CoffeeScript、ES6的支持;有独立的配置文件webpack.config.js;可以将代码切割成不同的chunk,实现按需加载,降低了初始化时间;支持 SourceUrls 和 SourceMaps,易于调试;具有强大的Plugin接口,大多是内部插件,使用起来比较灵活;webpack 使用异步 IO 并具有多级缓存。这使得 webpack 很快且在增量编译上更加快;webpack最常用与spa应用,主要是vue和React,其实它就非常像Browserify,但是将应用打包为多个文件。如果单页面应用有多个页面,那么用户只从下载对应页面的代码. 当他么访问到另一个页面, 他们不需要重新下载通用的代码。webpack也能用于服务端,但是构建后端代码一般都不会用webpack,坑太多了,所以正常情况下只用于前端webpack和gulp的区别gulp是基于流的构建工具:all in one的打包模式,输出一个js文件和一个css文件,优点是减少http请求,万金油方案。webpack是模块化管理工具,使用webpack可以对模块进行压缩、预处理、打包、按需加载等。webpack的特征?插件化:webpack本身非常灵活,提供了丰富的插件接口。基于这些接口,webpack开发了很多插件作为内置功能。速度快:webpack使用异步IO以及多级缓存机制。所以webpack的速度是很快的,尤其是增量更新。丰富的Loaders:loaders用来对文件做预处理。这样webpack就可以打包任何静态文件。高适配性:webpack同时支持AMD/CommonJs/ES6模块方案。webpack会静态解析你的代码,自动帮你管理他们的依赖关系。此外,webpack对第三方库的兼容性很好。代码拆分:webpack可以将你的代码分片,从而实现按需打包。这种机制可以保证页面只加载需要的JS代码,减少首次请求的时间。优化:webpack提供了很多优化机制来减少打包输出的文件大小,不仅如此,它还提供了hash机制,来解决浏览器缓存问题。开发模式友好:webpack为开发模式也提供了很多辅助功能。比如SourceMap、热更新等。使用场景多:webpack不仅适用于web应用场景,也适用于Webworkers、Node.js场景。基本的使用const path = require(‘path’);module.exports = { entry: “./app/entry”, // string | object | array // Webpack打包的入口 output: { // 定义webpack如何输出的选项 path: path.resolve(__dirname, “dist”), // string // 所有输出文件的目标路径 filename: “[chunkhash].js”, // string // 「入口(entry chunk)」文件命名模版 publicPath: “/assets/”, // string // 构建文件的输出目录 /* 其它高级配置 */ }, module: { // 模块相关配置 rules: [ // 配置模块loaders,解析规则 { test: /.jsx?$/, // RegExp | string include: [ // 和test一样,必须匹配选项 path.resolve(__dirname, “app”) ], exclude: [ // 必不匹配选项(优先级高于test和include) path.resolve(__dirname, “app/demo-files”) ], loader: “babel-loader”, // 模块上下文解析 options: { // loader的可选项 presets: [“es2015”] }, }, }, resolve: { // 解析模块的可选项 modules: [ // 模块的查找目录 “node_modules”, path.resolve(__dirname, “app”) ], extensions: [".js", “.json”, “.jsx”, “.css”], // 用到的文件的扩展 alias: { // 模块别名列表 “module”: “new-module” }, }, devtool: “source-map”, // enum // 为浏览器开发者工具添加元数据增强调试 plugins: [ // 附加插件列表 // … ],}以上简单配置主要有一下几点:Entry:指定webpack开始构建的入口模块,从该模块开始构建并计算出直接或间接依赖的模块或者库Output:告诉webpack如何命名输出的文件以及输出的目录Loaders:由于webpack只能处理javascript,所以我们需要对一些非js文件处理成webpack能够处理的模块,比如sass文件Plugins:Loaders将各类型的文件处理成webpack能够处理的模块,plugins有着很强的能力。插件的范围包括,从打包优化和压缩,一直到重新定义环境中的变量。但也是最复杂的一个。比如对js文件进行压缩优化的UglifyJsPlugin插件Chunk:coding split的产物,我们可以对一些代码打包成一个单独的chunk,比如某些公共模块,去重,更好的利用缓存。或者按需加载某些功能模块,优化加载时间。在webpack3及以前我们都利用CommonsChunkPlugin将一些公共代码分割成一个chunk,实现单独加载。在webpack4 中CommonsChunkPlugin被废弃,使用SplitChunksPlugin ...

December 31, 2018 · 1 min · jiezi

webpack源码分析之六:hot module replacement

前言在web开发中,webpack的hot module replacement(HMR)功能是可以向运行状态的应用程序定向的注入并更新已经改变的modules。它的出现可以避免像LiveReload那样,任意文件的改变而刷新整个页面。这个特性可以极大的提升开发拥有运行状态,以及资源文件普遍过多的前端应用型网站的效率。完整介绍可以看官网文档本文是先从使用者的角度去介绍这个功能,然后从设计者的角度去分析并拆分需要实现的功能和实现的一些细节。功能介绍对于使用者来说,体验到这个功能需要以下的配置。webpack.config.js:const path = require(‘path’);const HtmlWebpackPlugin = require(‘html-webpack-plugin’);const webpack = require(‘webpack’);module.exports = { entry: { app: ‘./src/index.js’ }, devServer: { contentBase: ‘./dist’, hot: true, }, plugins: [ new webpack.HotModuleReplacementPlugin(), new HtmlWebpackPlugin() ], output: { filename: ‘[name].bundle.js’, path: path.resolve(dirname, ‘dist’) }};代码: index.js 依赖print.js,使用module.hot.accept接受print.js的更新:import ‘./print’;if (module.hot) { module.hot.accept(’./print’, function () { console.log(‘i am updated’); })}改变print.js代码:console.log(‘print2’)console.log(‘i am change’);此时服务端向浏览器发送socket信息,浏览器收到消息后,开始下载以hash为名字的下载的json,jsonp文件,如下图:浏览器会下载对应的hot-update.js,并注入运行时的应用中:webpackHotUpdate(0,{// 30:// (function(module, exports) {console.log(‘print2’)console.log(‘i am change’);// })})0 代表着所属的chunkid,30代表着所属的moduleid。替换完之后,执行module.hot.accept的回调函数,如下图:简单来讲,开启了hmr功能之后,处于accepted状态的module的改动将会以jsonp的形式定向的注入到应用程序中。一张图来表示HMR的整体流程: 功能分析提出问题当翻开bundle.js的时候,你会发现Runtime代码多了许多以下的代码:// function hotDownloadUpdateChunk(chunkId) {// …// }// function hotDownloadManifest(requestTimeout) {// …// }/// function hotSetStatus(newStatus) {// …// }// 打包的时候,明明只引用了4个文件,但是整个打包文件却有30个modules之多:/ 30 /// (function(module, exports) {console.log(‘print3’)console.log(‘i am change’);/***/ })到现在你可能会有以下几个疑问:hmr模式下Runtime是怎么加上HMR Runtime代码的。业务代码并没有打包socketjs,hot代码的,浏览器为什么会有这些代码的。浏览器是如何判断并下载如:501eaf61104488775d2e.hot-update.json,。501eaf61104488775d2e.hot-update.js文件的,并且如何将js内容替换应用程序的内容。编译器如何监听资源文件的变化,并将改动的文件输出到Server里面供客户端下载,如501eaf61104488775d2e.hot-update.json,0.501eaf61104488775d2e.hot-update.js。服务器如何监听编译器的生命周期,并在其编译开始,结束的时候,向浏览器发送socket信息。浏览器替换新的module之后,如何及时清理缓存。分析问题Runtime代码是根据MainTemplate内部实现的,有多种场景如normal,jsonp,hot模式,则可以考虑将字符串拼接改成事件。编译开始时候,如果是hot模式,在编译器层面将socketjs,hot代码一并打包进来。监听文件变化,webpack 封装了watchpack模块去监听如window,linux,mac系统的文件变化编译结束后生成hash,文件变化后对比最近一次的hash。有变动则生成新的变动文件。server层监听编译器的编译开始,结束的事件,如compile,watch,done事件,触发事件后,像浏览器发送对应的websocket消息。浏览器接受到了websocket消息后,根据hash信息,得到[hash].hot-update.json文件,从中解析到chunkId,在根据chunkId,hash信息去下载[chunkId].[hash]-update.js。浏览器替换新的module之前,installedModules对象中删除缓存的module,在替换后,执行__webpack_require(id),将其并入到installedModules对象中。功能实现以上问题,可以从三个不同的角度去解决。server,webpack,brower。webpack-dev-server对入口entry做包装处理,如将entry:{app:’./src/index.js’},转换为entry:{app:[’/Users/zhujian/Documents/workspace/webpack/webpack-demos-master/node_modules/_webpack-dev-server@2.11.2@webpack-dev-server/client/index.js?http://localhost:8082’],‘webpack/hot/dev-server’,’./src/index.js’}构建业务代码时,附带上socketjs,hot代码。初始化服务端sockjs,并注册connection事件,向客户端发送hot信息,开启hmr功能。Server.js if (this.hot) this.sockWrite([conn], ‘hot’);浏览器hot: function hot() { _hot = true; log.info(’[WDS] Hot Module Replacement enabled.’); }监听编译器的生命周期模块。socket监听compiler的compile事件,通过webSocket向客户端发送invalid 信息监听compiler的done事件,通过webSocket向客户端发送still-ok,hash以及hash内容,并将所有请求资源文件设置为可用状态 compiler.plugin(‘compile’, invalidPlugin); compiler.plugin(‘invalid’, invalidPlugin); compiler.plugin(‘done’, (stats) => { this.sendStats(this.sockets, stats.toJson(clientStats)); this.stats = stats; });资源文件锁定监听compiler的invalid,watch-run,run事件。将所有请求资源文件设置为pending状态,直到构建结束。监听compiler的done事件,将所有请求资源文件重新设置为可用状态 context.compiler.plugin(“done”, share.compilerDone); context.compiler.plugin(“invalid”, share.compilerInvalid); context.compiler.plugin(“watch-run”, share.compilerInvalid); context.compiler.plugin(“run”, share.compilerInvalid);webpackTemplateMainTemplate增加module-obj,module-require事件module-obj事件负责生成以下代码// var module = installedModules[moduleId] = {// i: moduleId,// l: false,// exports: {},// hot: hotCreateModule(moduleId),// parents: (hotCurrentParentsTemp = hotCurrentParents, hotCurrentParents = [], hotCurrentParentsTemp),// children: []// };//module-require事件负责生成以下代码// modules[moduleId].call(module.exports, module, module.exports, hotCreateRequire(moduleId));Compiler新增Watching类支持watch模式,并结合watchpack监听文件变化。class Watching { ….}Module新增updateHash实现updateHash(hash) { this.updateHashWithSource(hash); this.updateHashWithMeta(hash); super.updateHash(hash); }Chunk新增updateHash实现 updateHash(hash) { hash.update(${this.id} ); hash.update(this.ids ? this.ids.join(",") : “”); hash.update(${this.name || ""} ); this.modules.forEach(m => m.updateHash(hash)); }Compilation增加createHash方法,默认调用md5计算compilation hash。调用依赖树module,chunk的updateHash方法。createHash() { ….}Parser增加对ifStatement的statement类的解析支持如:if(module.hot){}编译后if(true){}MultiEntryPlugin增加MultiEntryDependency,MultiModule,MultiModuleFactory。将数组的entry对象,打包为以下的资源文件。entry:{app:[’/Users/zhujian/Documents/workspace/webpack/webpack-demos-master/node_modules/webpack-dev-server@2.11.2@webpack-dev-server/client/index.js?http://localhost:8082’],‘webpack/hot/dev-server’,’./src/index.js’}打包后/* 5 //**/ (function(module, exports, webpack_require) {// webpack-dev-server/client/index.js__webpack_require(6); //webpack/hot/dev-server__webpack_require(26); // .src/index.jsmodule.exports = webpack_require(28);/***/ })HotModuleReplacementPlugin监听module-require,require-extensions,hash,bootstrap,current-hash,module-obj等事件生成HMR Runtime 代码监听record事件,存储最近一次的compilation hash。compilation.plugin(“record”, function(compilation, records) { if(records.hash === this.hash) return; records.hash = compilation.hash; records.moduleHashs = {}; this.modules.forEach(module => { const identifier = module.identifier(); const hash = require(“crypto”).createHash(“md5”); module.updateHash(hash); records.moduleHashs[identifier] = hash.digest(“hex”); }); records.chunkHashs = {}; this.chunks.forEach(chunk => { records.chunkHashs[chunk.id] = chunk.hash; }); records.chunkModuleIds = {}; this.chunks.forEach(chunk => { records.chunkModuleIds[chunk.id] = chunk.mapModules(m => m.id); }); });监听additional-chunk-assets 事件,对比record的最近一次hash,判断变化之后。生成以[hash].hot-update.json,[chunkId].[hash].hot-update.js为名称的assets对象。compilation.plugin(“additional-chunk-assets”, function() { …. this.assets[filename] = source; });Brower初始化runtime,将所有附加的模块代码统一增加parents,children等属性。并提供check,以及apply方法去管理hmr的生命周期。check,发送http请求请求并更新manifest,请求成功之后,会将待更新的chunk hash与当前chunk hash做比较。多个chunk,则会等待相应的chunk 完成下载之后,将状态转回ready状态,表示更新已准备并可用。apply,当应用状态为ready时,将所有待更新模块置为无效(清除客户端缓存),更新中调用新模块(更新缓存),更新完成之后,应用程序切回idle状态。初始化websocket,与server端建立长链接,并注册事件。如ok,invalid,hot,hash等事件。初始化hot 代码,注册事件对比新老hash,不相等则调用check方法开启模块更新功能。module.hot.check(true).then(function(updatedModules) { ….})代码实现本人的简易版webpack实现simple-webpack (完) ...

December 31, 2018 · 2 min · jiezi

webpack入门学习手记(四)

本人微信公众号:前端修炼之路,欢迎关注。再过一天,就是2019年了,在这里祝大家新年快乐,阖家欢乐,万事大吉。????????????在文章之前,简单的回顾一下2018主要完成的事情,与君分享,共同进步。????????????坚持健身????。这一年第一次坚持下了健身,每周最少1-2次。取得驾照????。这一年成为了合法的驾驶员,并拥有了人生第一辆私家车。考入燕大????????。这一年成功考入燕山大学继续教育学院专升本,为进一步提升学历打下基础。完成项目????????。这一年在家待业成为常态,期间接了私活,首次以个人能力完成项目。原创文章✍️。这一年开启了原创文章的写作,发布在微信公众号、SegmentFault、掘金上。学会滑雪????。这一年学会了单板滑雪,实现了多年前的愿望。以下是正文。管理输出之前的文章学习了如何加载资源,这一节学习如何将资源输出。对项目做一些修改,创建一个js文件。src/print.jsexport default function printMe() { console.log(‘I get called from print.js!’);}在程序入口文件中,引用这个文件。这个文件的内容,对上一节的代码做了修改。只保留了加载css样式的代码。src/index.jsimport _ from ’lodash’;import printMe from ‘./print.js’import ‘./style.css’;function component() { let element = document.createElement(‘div’); let btn = document.createElement(‘button’); // Lodash, currently included via a script, is required for this line to work element.innerHTML = _.join([‘Hello’, ‘webpack’], ’ ‘); element.classList.add(‘hello’); // add a button to print log btn.innerHTML = ‘Click me and check the console!’; btn.onclick = printMe; element.appendChild(btn); return element;}document.body.appendChild(component());修改配置文件,将新增加的文件进行打包。webpack.config.jsconst path = require(‘path’);module.exports = { entry: { app: ‘./src/index.js’, print: ‘./src/print.js’ // 也可以不打包这个文件,因为在index.js中已经引入了 }, output: { filename: ‘[name].bundle.js’, path: path.resolve(__dirname, ‘dist’) }, module: { rules: [{ test: /.css$/, use: [‘style-loader’, ‘css-loader’] }] }};这时,配置文件的entry入口,改成了对象形式,允许传入多个文件。其中的对象属性名app和print,在输出文件属性output.filename中以占位符[name]的形式展示。因为加载了css,所以添加了相应的loader。注意:其实在入口文件中,可以不将src/print.js文件打包,因为在打包生成的dist/app.bundle.js文件中,已经包含了src/print.js文件中的内容。在页面文件中引用新打包好的文件。dist/index.html<!doctype html> <html> <head> <title>Output Management</title> <script src="./print.bundle.js"></script> </head> <body> <script src="./app.bundle.js"></script> </body> </html>最后执行打包命令npm run build之后,会在dist目录发现新生成的打包文件。添加插件现在设想一下,假如修改了原始文件index.js和print.js的名字,该怎么办呢?难道手动一个个去修改文件名吗?如果文件数量扩大到20个呢?通过webpack插件可以很自动化的完成上面的需求。添加html-webpack-plugin和clean-webpack-plugin这两个插件。第一插件是用来生成html页面的,会自动将output.filename输出文件添加到页面中。第二个插件是用来清理/dist目录的,防止项目目录过于杂乱。首先安装这两个插件npm install –save-dev html-webpack-plugin clean-webpack-plugin然后修改配置文件,引用这两个插件。webpack.config.jsconst path = require(‘path’);const HtmlWebpackPlugin = require(‘html-webpack-plugin’);const CleanWebpackPlugin = require(‘clean-webpack-plugin’);module.exports = { entry: { app: ‘./src/index.js’, print: ‘./src/print.js’ // 也可以不打包这个文件,因为在index.js中已经引入了 }, output: { filename: ‘[name].bundle.js’, path: path.resolve(__dirname, ‘dist’) }, module: { rules: [{ test: /.css$/, use: [‘style-loader’, ‘css-loader’] }] }, plugins:[ new HtmlWebpackPlugin({title: ‘Output Management’}), new CleanWebpackPlugin([‘dist’]) ]};在plugins中,以数组的形式,添加了这两个插件。html-webpack-plugin接受的title属性,就是生成的html页面中title。clean-webpack-plugin接受的参数,就是要清理的目录名称。现在如果执行打包命令npm run build,会发现webpack先删除了dist目录,然后生成相应的文件。如果打开index.html文件,会发现我们在header部分引入的js文件,已经被webpack移动到了body。说明我们的配置文件生效了。这样的话,就解决了上面提到的问题,以后文件名字修改之后,只需要修改配置文件中相应名字,然后执行打包命令就可以了。以上就是指南手册中的Output Management部分。总结一下主要内容:打包多个入口文件,并输出到相应的文件中。通过插件自动化生成html页面,并添加相应引用文件。通过插件清理项目文件。下一篇笔记整理webpack官方文档的指南手册剩余部分,敬请关注。(待续)相关文章webpack入门学习手记(一)webpack入门学习手记(二)webpack入门学习手记(三)webpack入门学习手记(四) ...

December 30, 2018 · 1 min · jiezi

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

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

December 30, 2018 · 3 min · jiezi

➹使用webpack配置多页面应用MPA:100

使用webpack配置MPA为什么需要使用 webpack 构建多页应用呢?因为某些项目使用 SPA 不太合适(大多是 SEO 的原因),或者您在做项目时有其他的需求。如果你有如下需求:使用 ES6 进行开发期望使用面向对象开发(class)自动压缩合并 CSS 和 JS 文件使用 ESLint 进行代码检查自动生成 HTML 文件自动抽取 CSS 文件 …有了这些需求,基本上就必须使用 webpack 了。安装依赖首先是项目中需要使用的依赖安装。安装 webpack 和 webpack-dev-servernpm install webpack webpack-dev-server –save-dev安装 webpack-mergenpm install webpack-merge –save-dev该插件用来对 webpack 配置进行合并操作。安装 babel 相关插件npm install babel-core babel-loader babel-preset-env –save-dev这系列插件用来对 ES6 语法进行转换。安装样式处理相关插件npm install css-loader style-loader postcss-loader autoprefixer –save-dev这系列插件用来处理 CSS 样式,其中 autoprefixer 是 postcss 的一个插件,用来自动给 CSS 样式添加前缀。安装 file-loader该插件将在导入图片、字体等文件时发挥作用。PS.您也可以安装 url-loader 以实现相同的作用:npm install file-loader –save-devnpm install url-loader –save-dev安装 ESLint 相关的插件npm install eslint eslint-loader –save-dev这些插件用来对 JavaScript 代码进行检查。安装 html-webpack-plugin 插件npm install html-webpack-plugin –save-dev该插件用来自动生成 HTML 文件。安装 extract-text-webpack-plugin 插件npm install extract-text-webpack-plugin –save-dev该插件用来将 CSS 抽取到独立的文件。安装 clean-webpack-plugin 插件npm install clean-webpack-plugin –save-dev该插件用来对 dist 文件夹进行清理工作,每次打包时先清理之前的 dist 文件夹。下面是这些安装了的所有依赖:… “devDependencies”: { “autoprefixer”: “^7.1.3”, “babel-core”: “^6.26.0”, “babel-loader”: “^7.1.2”, “babel-preset-env”: “^1.6.0”, “clean-webpack-plugin”: “^0.1.16”, “css-loader”: “^0.28.7”, “eslint”: “^4.6.1”, “eslint-loader”: “^1.9.0”, “extract-text-webpack-plugin”: “^3.0.0”, “file-loader”: “^0.11.2”, “html-webpack-plugin”: “^2.30.1”, “postcss-loader”: “^2.0.6”, “style-loader”: “^0.18.2”, “url-loader”: “^0.5.9”, “webpack”: “^3.5.5”, “webpack-dev-server”: “^2.7.1”, “webpack-merge”: “^4.1.0” },…配置文件划分使用 webpack 进行项目构建时,我们有不同的目的,因此最好将配置文件进行拆分,以适应不同的工作:├─config│ config.js│ webpack.config.base.js│ webpack.config.dev.js│ webpack.config.lint.js│ webpack.config.prod.js│ webpack.config.js下面是一些配置的说明:config.js:一些全局的配置,比如 HTML 文件的路径、publicPath 等webpack.config.base.js:最基础的配置文件webpack.config.dev.js:开发环境配置文件webpack.config.lint.js:使用 ESLint 代码检查时的配置文件webpack.config.prod.js:生产环境配置文件webpack.config.js:主配置文件,根据环境变量引用相应的环境的配置这些配置文件之间是通过 webpack-merge 这个插件进行合并的。配置多页应用的关键点如何使用 webpack 配置多页面应用呢?实现多页面应用的关键点在哪里呢?首先需要简单看一下多页应用和单页应用功能的区别。单页应用的特点:只有一个入口页面(index.html)这个单页页面(index.html)中需要引入打包后的所有 JavaScript 文件所有的页面内容完全由 JavaScript 生成单页应用有自己的路由系统,服务器端没有和路由对应的文件···多页应用的特点:每个版块对应一个页面每个页面需要对公共的 JavaScript 进行引入每个页面还需要引入和其自身对应的 JavaScript 文件由于对应了多个页面,因此不是所有页面内容都是由 JavaScript 生成的没有自己的路由系统,服务器端有对应的静态文件···抛开生成页面内容和路由系统,我们可以看到单页应用和多页应用最大的区别就是:单页应用需要在入口页面引入所有的 JavaScript 文件多页应用需要在每个页面中引入公共的 JavaScript 文件以及其自身的 JavaScript 文件由于 CSS 文件是可以由 extract-text-webpack-plugin 这个插件自动提取并插入到 HTML 页面的,因此我们只需要关心如何在 HTML 页面中引入 JavaScript 文件了。webpack 在打包时,会将入口文件中的 JavaScript 文件打包到某个目标文件中,在不考虑代码分割提取的情况下,一个入口文件会打包为一个目标文件,多个入口文件会打包为多个对应的目标文件。因此,我们可以将每个多页页面中的特有的 JavaScript 文件作为入口文件,在打包时将对应打包成不同的 bundle 文件(结果文件),如果你想要的话,还可以在打包时进行代码分割处理,将公用代码抽取成一个文件,然后在 HTML 中引入这些 JavaScript 文件就好了。总结一下,使用 webpack 配置多页应用的关键点在于:将每个页面中特有的 JavaScript 文件作为入口文件进行打包在打包后,每个页面中都需要引入这些打包后的文件您可以在打包时进行公用代码提取,然后在 HTML 文件中引入说了这么多,其实就是利用了 webpack 多入口文件进行打包。自动生成 HTML 页面在使用 webpack 对 JavaScript 文件进行打包时,通常需要在打包的文件名中加一个 hash 字符串用来防止缓存,当我们修改了 JavaScript 代码后,打包后的文件名也会发生变化。此时如果手动在 HTML 中引用这些 JavaScript 文件,是非常麻烦的。因此,我们期望能自动生成 HTML 文件,并自动引用打包后的 JavaScript 文件。所谓自动生成 HTML 文件,可以理解为将源代码的 HTML 复制到目标文件夹中,同时自动引用打包后的 JavaScript 文件。要完成这项操作,就需要使用前面安装的 html-webpack-plugin 这个插件。html-webpack-plugin 插件的使用首先,在我的项目中,有这么一些 HTML 页面,将它们放在 html 文件夹中:Mode LastWriteTime Length Name—- ————- —— —–a—- 2017/9/5 18:04 1071 company_intro.html-a—- 2017/9/5 18:04 988 contact_us.html-a—- 2017/9/5 18:04 1131 cooperate.html-a—- 2017/9/5 18:04 1244 enterprise_culture.html-a—- 2017/9/5 18:04 1011 hornors.html-a—- 2017/9/5 18:04 1365 index.html-a—- 2017/9/5 18:04 1769 investment.html-a—- 2017/9/5 18:04 1005 join_us.html-a—- 2017/9/5 18:04 1037 news_center.html-a—- 2017/9/5 18:04 987 news_item.html-a—- 2017/9/5 18:04 1134 operate.html-a—- 2017/9/5 18:04 1255 product.html-a—- 2017/9/5 18:04 1132 schools.html然后,把这些 HTML 文件名(不要后缀)都写在 config.js 文件中,以供取用:module.exports = { HTMLDirs:[ “index”, “company_intro”, “enterprise_culture”, “hornors”, “news_center”, “news_item”, “product”, “schools”, “operate”, “cooperate”, “join_us”, “contact_us”, “investment” ], }HTMLDirs 是一个数组,其中保存了项目中会用到的所有 HTML 页面。接下来,每个 HTML 页面都对应一份 JavaScript 代码,因此在 js 文件夹中建立对应的 JavaScript 文件:Mode LastWriteTime Length Name—- ————- —— —–a—- 2017/9/5 18:04 2686 company_intro.js-a—- 2017/9/5 18:04 594 contact_us.js-a—- 2017/9/5 18:04 1725 cooperate.js-a—- 2017/9/8 16:54 3505 enterprise_culture.js-a—- 2017/9/5 18:04 2208 hornors.js-a—- 2017/9/8 16:54 4491 index.js-a—- 2017/9/5 18:04 3180 investment.js-a—- 2017/9/5 18:04 1327 join_us.js-a—- 2017/9/8 16:55 3689 news_center.js-a—- 2017/9/5 18:04 1972 news_item.js-a—- 2017/9/5 18:04 2728 operate.js-a—- 2017/9/5 18:04 2664 product.js-a—- 2017/9/5 18:04 2476 schools.js这两项是必须的,只有提供了每个页面的 HTML 文件和对应的 JavaScript 文件,才能构建多页面应用。同时,可能每个页面都有自己的样式,因此您也可以在 css 文件夹中建立一些样式文件:Mode LastWriteTime Length Name—- ————- —— —–a—- 2017/9/5 18:04 419 company_intro.css-a—- 2017/9/5 18:04 167 contact_us.css-a—- 2017/9/5 18:04 214 cooperate.css-a—- 2017/9/5 18:04 926 enterprise_culture.css-a—- 2017/9/5 18:04 255 hornors.css-a—- 2017/9/5 18:04 693 investment.css-a—- 2017/9/5 18:04 136 join_us.css-a—- 2017/9/5 18:04 541 news_center.css-a—- 2017/9/5 18:04 623 news_item.css-a—- 2017/9/5 18:04 342 operate.css-a—- 2017/9/5 18:04 236 product.css-a—- 2017/9/5 18:04 213 schools.css关于建立样式这一项,不是必须的。最后,我们就可以使用 html-webpack-plugin 这个插件来自动生成 HTML 文件了,html-webpack-plugin 插件的用法如下:// 引入插件const HTMLWebpackPlugin = require(“html-webpack-plugin”);// 引入多页面文件列表const { HTMLDirs } = require("./config");// 通过 html-webpack-plugin 生成的 HTML 集合let HTMLPlugins = [];// 入口文件集合let Entries = {}// 生成多页面的集合HTMLDirs.forEach((page) => { const htmlPlugin = new HTMLWebpackPlugin({ filename: ${page}.html, template: path.resolve(__dirname, ../app/html/${page}.html), chunks: [page, ‘commons’], }); HTMLPlugins.push(htmlPlugin); Entries[page] = path.resolve(__dirname, ../app/js/${page}.js);})在上面的代码中,首先引入了所需的插件和变量,然后利用 html-webpack-plugin 循环生成 HTML 页面。简单说下 HTMLWebpackPlugin 构造函数的几个参数:filename:生成的 HTML 文件名,我这里选择和原始文件名保持一致template:生成 HTML 文件使用的模板,也就是我们之前在 html 文件夹中建立的那些文件chunks:生成 HTML 文件时会自动插入相应的代码片段(也就是 JavaScript 文件),我这里选择插入每个页面对应的 JavaScript 文件,以及最后提取出来的公共文件代码块。关于 chunks 还需要说明一点,chunks 是一个数组,在生成 HTML 文件时会将数组中的对应的 JavaScript 片段自动插入到 HTML 中,这些片段也就是 webpack 打包时的 output 选项中的 [name]。这里只需要写上 [name] 值就行了,无需使用打包生成的完整名称,因为这会还没开始打包呢,打包后生成的名称咱也不知道。最后,我们把这些生成 HTML 文件的配置插入到 HTMLPlugins 这个数组中,同时设置 webpack 的入口文件。目录划分在这个脚手架中,我是这样划分项目结构的: ├─app │ ├─css │ ├─html │ ├─img │ ├─js │ └─lib ├─config └─dist ├─css ├─img └─js 其中 app 是项目的源码,config 是 webpack 相关的一些配置文件,dist 是存放打包后的文件,是由 webpack 自动生成的。 更详细的文件结构如下: │ .babelrc │ .eslintrc.js │ .gitignore │ package.json │ postcss.config.js │ webpack.config.js │ ├─app │ │ favicon.ico │ │ │ ├─css │ │ main.css │ │ │ ├─html │ │ index.html │ │ │ │ │ ├─img │ │ back.png │ │ │ ├─js │ │ ajax.js │ │ footer.js │ │ index.js │ │ nav.js │ │ public.js │ │ tity_nav.js │ │ │ └─lib │ flexible.js │ normalize.css │ swiper.css │ swiper.js │ └─config config.js webpack.config.base.js webpack.config.dev.js webpack.config.lint.js webpack.config.prod.jspackage.json所有的功能都是从 package.json 的 scripts 入口开始执行的,我想要脚手架有以下功能:开发环境构建生产环境构建ESLint 代码检查环境生产环境构建后的服务器预览环境在开发或代码检查环境,需要启用 webpack-dev-server 命令,生产环境构建需要启用 webpack 命令,预览环境需要启用 http-server 环境。上文介绍时把 http-server 给落下了,您现在可以进行如下安装:npm install http-server –save-devscripts 命令行配置如下: “scripts”: { “dev”: “set NODE_ENV=dev && webpack-dev-server –open”, “build”: “set NODE_ENV=prod && webpack -p”, “lint”: “set NODE_ENV=lint && webpack-dev-server –open”, “serve”: “http-server ./dist -p 8888 -o”, “serve2”: “http-server ./dist -p 8888” },下面是整个 package.json 文件:{ “name”: “xxx”, “version”: “1.0.0”, “description”: “”, “main”: “index.js”, “scripts”: { “dev”: “set NODE_ENV=dev && webpack-dev-server –open”, “build”: “set NODE_ENV=prod && webpack -p”, “lint”: “set NODE_ENV=lint && webpack-dev-server –open”, “serve”: “http-server ./dist -p 8888 -o”, “serve2”: “http-server ./dist -p 8888” }, “author”: “”, “license”: “ISC”, “devDependencies”: { “autoprefixer”: “^7.1.3”, “babel-core”: “^6.26.0”, “babel-loader”: “^7.1.2”, “babel-plugin-transform-es2015-spread”: “^6.22.0”, “babel-preset-env”: “^1.6.0”, “clean-webpack-plugin”: “^0.1.16”, “css-loader”: “^0.28.7”, “eslint”: “^4.5.0”, “eslint-loader”: “^1.9.0”, “extract-text-webpack-plugin”: “^3.0.0”, “file-loader”: “^0.11.2”, “html-webpack-plugin”: “^2.30.1”, “http-server”: “^0.10.0”, “postcss-loader”: “^2.0.6”, “style-loader”: “^0.18.2”, “url-loader”: “^0.5.9”, “webpack”: “^3.5.5”, “webpack-dev-server”: “^2.7.1”, “webpack-merge”: “^4.1.0” }, “dependencies”: {}}启用环境如果您想启用某个环境,需要使用 npm run xxx 命令:npm run dev:进入开发环境npm run build:进入生产环境npm run lint:执行代码检查npm run serve:服务器环境下预览(打开浏览器)npm run serve2:服务器环境下预览(不打开浏览器)默认情况下,使用这些命令都会先引入和 package.js 同目录下的 webpack.config.js 文件。由于我们不会将所有的配置都放在 webpack.config.js 中,而是过环境变量进行区分,在 webpack.config.js 中引用其他的配置文件。设置环境变量采用的语法:set NODE_ENV=xxx这里我们为开发、生产、代码检查和预览这几个环境设置了环境变量。webpack.config.jswebpack.config.js 文件比较简单,只有两行代码,其作用就是用来引用其他的配置文件:// 获取环境命令,并去除首尾空格const env = process.env.NODE_ENV.replace(/(\s*$)|(^\s*)/ig,"");// 根据环境变量引用相关的配置文件module.exports = require(./config/webpack.config.${env}.js)webpack.config.base.jswebpack.config.base.js 是最基础的配置文件,包含了这些环境都可能使用到的配置。1)相关插件引入const path = require(“path”);// 引入插件const HTMLWebpackPlugin = require(“html-webpack-plugin”);// 清理 dist 文件夹const CleanWebpackPlugin = require(“clean-webpack-plugin”)// 抽取 cssconst ExtractTextPlugin = require(“extract-text-webpack-plugin”);#### 2)自动生成 HTML 的配置// 引入多页面文件列表const config = require("./config");// 通过 html-webpack-plugin 生成的 HTML 集合let HTMLPlugins = [];// 入口文件集合let Entries = {}// 生成多页面的集合config.HTMLDirs.forEach((page) => { const htmlPlugin = new HTMLWebpackPlugin({ filename: ${page}.html, template: path.resolve(__dirname, ../app/html/${page}.html), chunks: [page, ‘commons’], }); HTMLPlugins.push(htmlPlugin); Entries[page] = path.resolve(__dirname, ../app/js/${page}.js);})3)主配置文件一览module.exports = { // 入口文件 entry:Entries, // 启用 sourceMap devtool:“cheap-module-source-map”, // 输出文件 output:{}, // 加载器 module:{ rules:[ ], }, // 插件 plugins:[],}4)配置 css 加载器{ // 对 css 后缀名进行处理 test:/.css$/, // 不处理 node_modules 文件中的 css 文件 exclude: /node_modules/, // 抽取 css 文件到单独的文件夹 use: ExtractTextPlugin.extract({ fallback: “style-loader”, // 设置 css 的 publicPath publicPath: config.cssPublicPath, use: [{ loader:“css-loader”, options:{ // 开启 css 压缩 minimize:true, } }, { loader:“postcss-loader”, } ] })},这里有两点需要说明:A.publicPath:在 css 中设置背景图像的 url 时,经常会找不到图片(默认会在 css 文件所在的文件夹中寻找),这里设置 extract-text-webpack-plugin 插件的 publicPath 为图片文件夹所在的目录,就可以顺利找到图片了。在 config.js 中,设置 cssPublicPath 的值:cssPublicPath:"../“B.postcss 我主要用来自动添加 css 前缀以及一点美化操作,在使用 postcss 时,需要在 postcss.config.js 中进行配置:module.exports = { plugins: { ‘autoprefixer’: { browsers: [’last 5 version’,‘Android >= 4.0’], //是否美化属性值 默认:true cascade: true, //是否去掉不必要的前缀 默认:true remove: true } } } 5)配置 js 加载器js 加载器的配置如下:{ test: /.js$/, exclude: /node_modules/, use: { loader: ‘babel-loader’, options: { presets: [’env’] } }},6)配置图片加载器图片加载器的配置如下:{ test: /.(png|svg|jpg|gif)$/, use:{ loader:“file-loader”, options:{ // 打包生成图片的名字 name:"[name].[ext]”, // 图片的生成路径 outputPath:config.imgOutputPath } }},outputPath 规定了输出图片的位置,默认情况下,图片在打包时会和所有的 HTML/CSS/JS 文件打包到一起,通过设置 outputPath 值可以将所有的图片都打包到一个单独的文件中。设置 config.js 的 imgOutputPath:imgOutputPath:“img/",在打包时,会将所有的图片打包到 dist 文件夹下的 img 文件夹中。7)配置自定义字体加载器自定义字体加载器的配置如下:{ test: /.(woff|woff2|eot|ttf|otf)$/, use:[“file-loader”]}8)插件配置插件配置如下:plugins:[ // 自动清理 dist 文件夹 new CleanWebpackPlugin([“dist”]), // 将 css 抽取到某个文件夹 new ExtractTextPlugin(config.cssOutputPath), // 自动生成 HTML 插件 …HTMLPlugins],同打包图片,在抽取 css 时也可以指定抽取的目录,只需将路径传入 extract-text-webpack-plugin 插件的构造函数中。配置 config.js 的 cssOutputPath 选项:cssOutputPath:”./css/styles.css",这里将所有的 css 提取到 dist 文件夹下的 css 文件夹中,并命名为 style.css。webpack.config.base.js 详细配置下面是 webpack.config.base.js 的详细配置文件:const path = require(“path”);// 引入插件const HTMLWebpackPlugin = require(“html-webpack-plugin”);// 清理 dist 文件夹const CleanWebpackPlugin = require(“clean-webpack-plugin”)// 抽取 cssconst ExtractTextPlugin = require(“extract-text-webpack-plugin”);// 引入多页面文件列表const config = require("./config");// 通过 html-webpack-plugin 生成的 HTML 集合let HTMLPlugins = [];// 入口文件集合let Entries = {}// 生成多页面的集合config.HTMLDirs.forEach((page) => { const htmlPlugin = new HTMLWebpackPlugin({ filename: ${page}.html, template: path.resolve(__dirname, ../app/html/${page}.html), chunks: [page, ‘commons’], }); HTMLPlugins.push(htmlPlugin); Entries[page] = path.resolve(__dirname, ../app/js/${page}.js);})module.exports = { entry:Entries, devtool:“cheap-module-source-map”, output:{ filename:“js/[name].bundle.[hash].js”, path:path.resolve(__dirname,"../dist") }, // 加载器 module:{ rules:[ { // 对 css 后缀名进行处理 test:/.css$/, // 不处理 node_modules 文件中的 css 文件 exclude: /node_modules/, // 抽取 css 文件到单独的文件夹 use: ExtractTextPlugin.extract({ fallback: “style-loader”, // 设置 css 的 publicPath publicPath: config.cssPublicPath, use: [{ loader:“css-loader”, options:{ // 开启 css 压缩 minimize:true, } }, { loader:“postcss-loader”, } ] }) }, { test: /.js$/, exclude: /node_modules/, use: { loader: ‘babel-loader’, options: { presets: [’env’] } } }, { test: /.(png|svg|jpg|gif)$/, use:{ loader:“file-loader”, options:{ // 打包生成图片的名字 name:"[name].[ext]", // 图片的生成路径 outputPath:config.imgOutputPath } } }, { test: /.(woff|woff2|eot|ttf|otf)$/, use:[“file-loader”] } ], }, plugins:[ // 自动清理 dist 文件夹 new CleanWebpackPlugin([“dist”]), // 将 css 抽取到某个文件夹 new ExtractTextPlugin(config.cssOutputPath), // 自动生成 HTML 插件 …HTMLPlugins ],}webpack.config.dev.js这个配置文件主要用来在开发环境使用,需要 webpack-dev-server 这个插件提供支持。该文件的配置如下:// 引入基础配置文件const webpackBase = require("./webpack.config.base");// 引入 webpack-merge 插件const webpackMerge = require(“webpack-merge”);// 引入配置文件const config = require("./config");// 合并配置文件module.exports = webpackMerge(webpackBase,{ // 配置 webpack-dev-server devServer:{ // 项目根目录 contentBase:config.devServerOutputPath, // 错误、警告展示设置 overlay:{ errors:true, warnings:true } }});其中,webpack-merge 这个插件用来对配置文件进行合并,在 webpack.config.base.js 的基础上合并新的配置。devServer 配置项的 contentBase 项是项目的根目录,也就是我们的 dist 目录,区别在于这个 dist 目录不是硬盘上的 dist 目录,而是存在于内存中的 dist 目录。在使用 webpack-dev-server 时,将会以这个内存中的 dist 目录作为根目录。devServer 的 overlay 选项中设置了展示错误和警告,这样当代码发生错误时,会将错误信息投射到浏览器上,方便我们开发。这里将 contentBase 指向了 config 中的一个配置:devServerOutputPath:"../dist",webpack.config.prod.js该配置文件用来在生产环境启用,主要用来压缩、合并和抽取 JavaScript 代码,并将项目文件打包至硬盘上的 dist 文件夹中。// 引入基础配置const webpackBase = require("./webpack.config.base");// 引入 webpack-merge 插件const webpackMerge = require(“webpack-merge”);// 引入 webpackconst webpack = require(“webpack”);// 合并配置文件module.exports = webpackMerge(webpackBase,{ plugins:[ // 代码压缩 new webpack.optimize.UglifyJsPlugin({ // 开启 sourceMap sourceMap: true }), // 提取公共 JavaScript 代码 new webpack.optimize.CommonsChunkPlugin({ // chunk 名为 commons name: “commons”, filename: “[name].bundle.js”, }), ]});在抽取公共的 JavaScript 代码时,我们将公共代码抽取为 commons.bundle.js,这个公共代码的 chunk(name)名就是 commons,在使用 html-webpack-plugin 自动生成 HTML 文件时会引用这个 chunk。webpack.config.lint.js这项配置用来进行代码检查,配置如下:const webpackBase = require("./webpack.config.base");const webpackMerge = require(“webpack-merge”);const config = require("./config");module.exports = webpackMerge(webpackBase,{ module:{ rules:[ { test: /.js$/, // 强制先进行 ESLint 检查 enforce: “pre”, // 不对 node_modules 和 lib 文件夹中的代码进行检查 exclude: /node_modules|lib/, loader: “eslint-loader”, options: { // 启用自动修复 fix:true, // 启用警告信息 emitWarning:true, } }, ] }, devServer:{ contentBase:config.devServerOutputPath, overlay:{ errors:true, warnings:true } }});在使用 eslint-loader 时,我们设置了 enforce:“pre” 选项,这个选项表示在处理 JavaScript 之前先启用 ESLint 代码检查,然后再使用 babel 等 loader 对 JavaScript 进行编译。在 eslint-loader 的 options 选项中,设置了自动修复和启用警告信息,这样当我们的代码出现问题时,ESLint 会首先尝试自动修复(如将双引号改为单引号),对于无法自动修复的问题,将以警告或错误的信息进行展示。配置 .eslintrc.js要想使用 ESLint 进行代码检查,除了使用 eslint-loader 之外,还需针对 ESLint 本身进行配置,这就需要一个 .eslintrc.js 文件。该文件的配置如下:module.exports = { env: { browser: true, commonjs: true, es6: true, node: true, }, extends: ’eslint:recommended’, parserOptions: { sourceType: ‘module’, }, rules: { ‘comma-dangle’: [’error’, ‘always-multiline’], indent: [’error’, 2], ’linebreak-style’: [’error’, ‘unix’], quotes: [’error’, ‘single’], semi: [’error’, ‘always’], ’no-unused-vars’: [‘warn’], ’no-console’: 0, },};package.json{ “name”: “xxx”, “version”: “1.0.0”, “description”: “”, “main”: “index.js”, “scripts”: { “dev”: “set NODE_ENV=dev && webpack-dev-server –open”, “build”: “set NODE_ENV=prod && webpack -p”, “lint”: “set NODE_ENV=lint && webpack-dev-server –open”, “serve”: “http-server ./dist -p 8888 -o”, “serve2”: “http-server ./dist -p 8888” }, “author”: “”, “license”: “ISC”, “devDependencies”: { “autoprefixer”: “^7.1.3”, “babel-core”: “^6.26.0”, “babel-loader”: “^7.1.2”, “babel-plugin-transform-es2015-spread”: “^6.22.0”, “babel-preset-env”: “^1.6.0”, “clean-webpack-plugin”: “^0.1.16”, “css-loader”: “^0.28.7”, “eslint”: “^4.5.0”, “eslint-loader”: “^1.9.0”, “extract-text-webpack-plugin”: “^3.0.0”, “file-loader”: “^0.11.2”, “html-webpack-plugin”: “^2.30.1”, “http-server”: “^0.10.0”, “postcss-loader”: “^2.0.6”, “style-loader”: “^0.18.2”, “url-loader”: “^0.5.9”, “webpack”: “^3.5.5”, “webpack-dev-server”: “^2.7.1”, “webpack-merge”: “^4.1.0” }, “dependencies”: {}}.gitignorenode_modulesdistnpm-debug.log.babelrc{ “plugins”: [“transform-es2015-spread”]}.eslintrc.jsmodule.exports = { env: { browser: true, commonjs: true, es6: true, node: true, }, extends: ’eslint:recommended’, parserOptions: { sourceType: ‘module’, }, rules: { ‘comma-dangle’: [’error’, ‘always-multiline’], indent: [’error’, 2], ’linebreak-style’: [’error’, ‘unix’], quotes: [’error’, ‘single’], semi: [’error’, ‘always’], ’no-unused-vars’: [‘warn’], ’no-console’: 0, },};postcss.config.jsmodule.exports = { plugins: { ‘autoprefixer’: { browsers: [’last 5 version’,‘Android >= 4.0’], //是否美化属性值 默认:true cascade: true, //是否去掉不必要的前缀 默认:true remove: true } } } config.jsmodule.exports = { HTMLDirs:[ “index”, “company_intro”, “enterprise_culture”, “hornors”, “news_center”, “news_item”, “product”, “schools”, “operate”, “cooperate”, “join_us”, “contact_us”, “investment” ], cssPublicPath:"../", imgOutputPath:“img/”, cssOutputPath:"./css/styles.css", devServerOutputPath:"../dist",}webpack.config.js// 获取环境命令,并去除首尾空格const env = process.env.NODE_ENV.replace(/(\s*$)|(^\s*)/ig,"");// 根据环境变量引用相关的配置文件module.exports = require(./config/webpack.config.${env}.js)webpack.config.base.jsconst path = require(“path”);// 引入插件const HTMLWebpackPlugin = require(“html-webpack-plugin”);// 清理 dist 文件夹const CleanWebpackPlugin = require(“clean-webpack-plugin”)// 抽取 cssconst ExtractTextPlugin = require(“extract-text-webpack-plugin”);// 引入多页面文件列表const config = require("./config");// 通过 html-webpack-plugin 生成的 HTML 集合let HTMLPlugins = [];// 入口文件集合let Entries = {}// 生成多页面的集合config.HTMLDirs.forEach((page) => { const htmlPlugin = new HTMLWebpackPlugin({ filename: ${page}.html, template: path.resolve(__dirname, ../app/html/${page}.html), chunks: [page, ‘commons’], }); HTMLPlugins.push(htmlPlugin); Entries[page] = path.resolve(__dirname, ../app/js/${page}.js);})module.exports = { entry:Entries, devtool:“cheap-module-source-map”, output:{ filename:“js/[name].bundle.[hash].js”, path:path.resolve(__dirname,"../dist") }, // 加载器 module:{ rules:[ { // 对 css 后缀名进行处理 test:/.css$/, // 不处理 node_modules 文件中的 css 文件 exclude: /node_modules/, // 抽取 css 文件到单独的文件夹 use: ExtractTextPlugin.extract({ fallback: “style-loader”, // 设置 css 的 publicPath publicPath: config.cssPublicPath, use: [{ loader:“css-loader”, options:{ // 开启 css 压缩 minimize:true, } }, { loader:“postcss-loader”, } ] }) }, { test: /.js$/, exclude: /node_modules/, use: { loader: ‘babel-loader’, options: { presets: [’env’] } } }, { test: /.(png|svg|jpg|gif)$/, use:{ loader:“file-loader”, options:{ // 打包生成图片的名字 name:"[name].[ext]", // 图片的生成路径 outputPath:config.imgOutputPath } } }, { test: /.(woff|woff2|eot|ttf|otf)$/, use:[“file-loader”] } ], }, plugins:[ // 自动清理 dist 文件夹 new CleanWebpackPlugin([“dist”]), // 将 css 抽取到某个文件夹 new ExtractTextPlugin(config.cssOutputPath), // 自动生成 HTML 插件 …HTMLPlugins ],}webpack.config.dev.js// 引入基础配置文件const webpackBase = require("./webpack.config.base");// 引入 webpack-merge 插件const webpackMerge = require(“webpack-merge”);// 引入配置文件const config = require("./config");// 合并配置文件module.exports = webpackMerge(webpackBase,{ // 配置 webpack-dev-server devServer:{ // 项目根目录 contentBase:config.devServerOutputPath, // 错误、警告展示设置 overlay:{ errors:true, warnings:true } }});webpack.config.prod.js// 引入基础配置const webpackBase = require("./webpack.config.base");// 引入 webpack-merge 插件const webpackMerge = require(“webpack-merge”);// 引入 webpackconst webpack = require(“webpack”);// 合并配置文件module.exports = webpackMerge(webpackBase,{ plugins:[ // 代码压缩 new webpack.optimize.UglifyJsPlugin({ // 开启 sourceMap sourceMap: true }), // 提取公共 JavaScript 代码 new webpack.optimize.CommonsChunkPlugin({ // chunk 名为 commons name: “commons”, filename: “[name].bundle.js”, }), ]});webpack.config.lint.jsconst webpackBase = require("./webpack.config.base");const webpackMerge = require(“webpack-merge”);const config = require("./config");module.exports = webpackMerge(webpackBase,{ module:{ rules:[ { test: /.js$/, // 强制先进行 ESLint 检查 enforce: “pre”, // 不对 node_modules 和 lib 文件夹中的代码进行检查 exclude: /node_modules|lib/, loader: “eslint-loader”, options: { // 启用自动修复 fix:true, // 启用警告信息 emitWarning:true, } }, ] }, devServer:{ contentBase:config.devServerOutputPath, overlay:{ errors:true, warnings:true } }});项目结构│ .babelrc│ .eslintrc.js│ .gitignore│ package.json│ postcss.config.js│ webpack.config.js│ ├─app│ │ favicon.ico│ │ │ ├─css│ │ main.css│ │ │ ├─html│ │ index.html│ │ │ │ │ ├─img│ │ back.png│ │ │ ├─js│ │ ajax.js│ │ footer.js│ │ index.js│ │ nav.js│ │ public.js│ │ tity_nav.js│ │ │ └─lib│ flexible.js│ normalize.css│ swiper.css│ swiper.js│ └─config config.js webpack.config.base.js webpack.config.dev.js webpack.config.lint.js webpack.config.prod.js注释:想引用jquery而不是每个页面都引用只需要:引入jquery后如果开启lint检查模式 可以正常使用的前提是每个页面都require 一次webpack.config.base.jscnpm i jquery –saveconst ProvidePlugin = new webpack.ProvidePlugin({ $: ‘jquery’, jQuery: ‘jquery’,});引入sasswebpack.config.base.js{ // s?css => scss或者css test:/.s?css$/, // 不处理 node_modules 文件中的 css 文件 exclude: /node_modules/, // 抽取 css 文件到单独的文件夹 use: ExtractTextPlugin.extract({ fallback: “style-loader”, // 设置 css 的 publicPath publicPath: config.cssPublicPath,//在 css 中设置背景图像的 url 时,经常会找不到图片(默认会在 css 文件所在的文件夹中寻找),这里设置 extract-text-webpack-plugin 插件的 publicPath 为图片文件夹所在的目录,就可以顺利找到图片了 use: [{ loader:“css-loader”, options:{ // 开启 css 压缩 minimize:true, } }, { loader:“postcss-loader”, }, { loader:“sass-loader”, //启用sass 虽然在这只写了sass-loader 但还要下载node-sass } ] })},如果import了类似swiper这种库函数 但不处理modules里面的swiper,所以这里要允许除了node_modules里面的swiper的其他所有文件webpack.config.base.js{ test: /.js$/, exclude: /^node_modules*swiper$/, use: { loader: ‘babel-loader’, options: { presets: [’env’] } }},➳ github地址:点我 ...

December 29, 2018 · 10 min · jiezi

Idea或webstrom配置webpack设置路径alias实现代码自动补全功能

做开发的时候,每次import想要代码提示,通过../../src/components长长的相对路径用起来很不方便,通过配置webpack的alias就可以让ide实现代码自动补全功能配置文件内容alias.config.jsconst path = require(‘path’);module.exports = { resolve: { alias: { ‘@’: path.resolve(__dirname, “src”), ‘_c’: path.resolve(__dirname, “src/components”), } }};idea 配置这样就能很愉快的跑起来了

December 29, 2018 · 1 min · jiezi

Vuex入门简单易懂系列(一)

在 学习vue 开发中,组件通信一直是一大痛点。当项目是很简单的 SPA 或者多入口项目时,可以靠着 vue 自带的 prop/$emit 进行组件通信;规模再大一些,可以搭配使用 bus 总线进行兄弟组件通信;项目再大一些,出现更复杂的组件关系时,复杂的组件通信可以让你写得怀疑人生。万幸的是, vue 官方出品了 vuex ,通过全局式的状态管理,解决了这一痛点。虽然 vuex 很好用,但是,很多小伙伴和我吐槽 vuex 的文档和 vue-ssr 的文档一样,让人看得一脸懵逼。安装并引入正常情况下,我们使用 vue-cli3 生成项目时,可以选择集成 vuex 到项目中。此时, vue-cli3 会自动安装 vuex ,并在 src 文件夹下生成 store.js 完成 vuex 的引入和配置。但是,很多同学并没有使用 vue-cli3 或者生成项目时没有选择集成 vuex 。此时,就只能手动安装并引入 vuex 了。安装由于 vuex 是用于全局状态管理的,所以,它不仅仅作用于开发环境,而且还要用于生产环境。显而易见,安装 vuex 应该使用 -S 即 –save 命令。npm install vuex -S引入类似于 vue-cli3 生成的项目,我们在 src 文件夹下新建 store.js ,并在其中写入:// store.jsimport Vue from ‘vue’import Vuex from ‘vuex’Vue.use(Vuex)export default new Vuex.Store({})然后,我们只需要在 vue 实例中引入 store.js 中的 Vuex.Store 实例即可:// main.jsimport Vue from ‘vue’import App from ‘./App.vue’import router from ‘./router’import store from ‘./store’Vue.config.productionTip = falsenew Vue({ router, // 引入store store, render: h => h(App)}).$mount(’#app’)Vuex的使用完成了 vuex 的安装和引入,接下来我们进入 Vuex 的使用。vuex 中有三要素: state, mutation 以及 action 。它们之间的关系可以用官网那张著名的图来表示:重点来了 (State)简单来说, state 表示状态,类似于 vue 中的 data (其实本质上就是差不多的, vuex 在 vue 的 beforeCreate 钩子中将 state 混入进 data)。但是,它们又有很大的不同: 在使用者看来, state 是全局的,这得益于 vuex 的设计理念——单一状态树。这些我将在后几篇文章中详细,现在我们只需要知道 state 是类似于全局下的 data 。接下来我们通过一个简单例子来感受下 state :首先,我们需要修改 store.js 文件,配置 state 。可以看到,我们在生成 Vuex.Store 实例时传入了实例化选项对象,对象包含一个 state 属性, state 对象的属性就是我们定义的全局状态。此时,我们定义了一个全局状态——count ,并将其的初始值设为1。// store.jsimport Vue from ‘vue’import Vuex from ‘vuex’Vue.use(Vuex)export default new Vuex.Store({ // 添加state state: { count: 1 }})接下来,我们需要在组件中引用 count,由于它是全局状态,我们可以在任何一个组件中使用。为了展示其威力,我们在两个不同的组件中使用它。首先我们在 App.vue 中使用它:在模板中,我们使用 $store.state.count 引入该全局状态,没错,使用它就是那么简单,只需要 以 $store.state.key 的形式调用。// App.vue<template> <div id=“app”> <div id=“nav”> {{$store.state.count}} <router-link to="/">Home</router-link> | <router-link to="/about">About</router-link> </div> <router-view/> </div></template>可以发现, Home 前多出了一个 1 ,这代表着我们成功引入了全局状态 count 。接下来我们在 Home.vue 的子组件 HelloWorld.vue 中引入 count 。相同的引用方式: $store.state.count// HelloWorld.vue<template> <div class=“hello”> {{$store.state.count}} </div></template>可以发现,success。Mutation但是,上面的示例有个问题,那就是全局状态是静态的。如果在实际应用场景中,一般来说,会经常更改状态。有的同学会说,我们直接在方法中修改 this.$store.state.key 的值不就行了吗?不好意思,当然是不行的。state 和 data 的另一大区别在于,你不能直接改变 state 。改变 store 中的状态的唯一途径就是显式地提交 (commit) mutation。这样使得我们可以方便地跟踪每一个状态的变化,从而让我们能够实现一些工具帮助我们更好地了解我们的应用。简而言之,我们把 mutation 当做接收 state 作为参数并修改 state 的自定义事件即可,上一段所说的 commit 就是触发 mutaion 这个自定义事件的方法。光说不练假把式,接下来,我们对为 vuex 添加上 mutation ,实现 state 的动态改变:首先,当然是修改生成 Vuex.Store 示例的选项对象,为其添加 mutations 。import Vue from ‘vue’import Vuex from ‘vuex’Vue.use(Vuex)export default new Vuex.Store({ state: { count: 1 }, // 添加mutation mutations: { increment (state) { state.count++ } }})在上面的代码中,我们添加了一个名为 increment 的 mutation 。完成了自定义事件,接下来,我们只需要在组件中对 mutation 进行触发即可。我们在 HelloWorld.vue 添加一个按钮,每次点击触发一次 increment 这个 mutation 。可以发现,触发方式很简单,只需要调用 store 自带的 commit 方法,其中参数为需要触发的 mutation 的名称。// HelloWorld.vue<template> <div class=“hello”> <div>{{$store.state.count}}</div> <button @click="$store.commit(‘increment’)">修改count</button> </div></template>点击页面中的按钮,你会发现,页面中的两个 count 都同时增加了1,说明我们成功实现了 state 的动态修改。Actionaction 类似于 mutation ,也相当于一种自定义事件。只不过, action 操作的是 mutation 而不是 state 。添加 action 的方法类似,在选项对象中新增 action 属性即可。与 mutation 的参数不同, action 的参数就是当前创建的 Vue.store 对象实例的上下文,一般将其命名为 context 。我们需要使用其自带的 commit 方法来触发 mutation 。下面我通过实际的例子来尝试下 action :首先,修改选项对象,使得新添加的 action 可以触发之前的 mutation :// store.jsimport Vue from ‘vue’import Vuex from ‘vuex’Vue.use(Vuex)export default new Vuex.Store({ state: { count: 1 }, mutations: { increment (state) { state.count++ } }, actions: { increment ({ commit }) { commit(‘increment’) } }})由于我们一般来说仅仅需要 context 中的 commit 方法,所以可以采用解构的方式,直接调用 commit 方法,而不需要以 context.commit 的方式使用它。接下来,只需要修改 HelloWorld.vue ,使其能够在点击按钮时触发即可。action 的触发方式和 mutation 类似,只不过调用的方法是 dispatch 。// HelloWorld.vue<template> <div class=“hello”> <div>{{$store.state.count}}</div> <button @click="$store.dispatch(‘increment’)">修改count</button> </div></template>点击页面按钮,你会发现,实现了和之前相同的效果。总结学会了 vuex 三jian客: state , mutation , action ,我们再回过头看看前面的那张关系图,此时应该很容易理解了吧?组件交互触发 action , 在 action 中进行异步操作(可选)并触发 mutation , mutation 控制 state 的变动, state 修改之后,触发响应式,重新渲染组件。PS(其他的进阶用法,如: getter , module , 简写以及 vuex 项目结构优化,甚至 vuex 源码解析将会在之后的文章一一讲解)最后个人认为对很多新手在入门时(不管学习什么东西),无可厚非最好刚开始是看视频。So,一份付出一份收获。如果你有需要购买学习视频的可以访问该网站加群http://www.wantmore.top,也可以直接添加群号:157285015。偶尔会分享学习干货噢! ...

December 27, 2018 · 2 min · jiezi

浅探前端图片优化

性能优化是前端开发必不可少的一环,而图片优化又是性能优化中必不可少的一环,但不知道有多少开发者在网页的开发过程中会注意图片的使用,图片使用不当可能会导致网页加载卡顿、网页加载速度慢等问题,这篇文章将会将我以往对图片的处理做个总结。不同格式图片优劣对比有人可能会问说好的图片优化呢?怎么说到图片格式了,其实在不同的场景选择使用不同格式的图片就是对图片的一种优化,这是最直接最重要但是最容易被忽略的,现在网页中常用的图片格式有JPG.PNG.SVG.WebP等,接下来我们就来介绍它们有何优劣JPGJPG格式的图片应该是使用场景最多的图片的格式了,由于JPG格式采用了极其高效的压缩算法,使其能在压缩50%甚至60%的情况下依旧可以保持不错的图片质量,因此在网站设计中使用类似背景图,轮播图等大图时都会考虑使用JPG格式的图片,但是JPG始终是有损压缩,在对线条感较强或者颜色比较丰富的图片做人为压缩时,可能会出现失真的情况,同时它也不支持透明度处理PNGPNG格式的图片特点大家都知道,就是高保真无损压缩,当对图片设计有较高要求时,首选PNG格式,显示高清细腻,但是它也有明显的问题就是体积过大SVGSVG格式图片有个显著特点就是它是可编程的,是基于xml语法的,同时作为矢量图,它可以无限放大而不变形,因此可以方便的对不同手机屏幕做自适应,相比于PNG和JPG它的体积更小,只有1kb甚至更小,但是它最大的缺陷就是渲染成本过高,因此我们在选择一些小且色彩单一的图标时可以考虑使用SVG格式的图片,如图一般情况下,我们会将SVG格式的图片上传到iconfont上,这样不仅方便管理而且方便使用,同时iconfont上还有许多其他设计师设计的优秀小图标可以直接拿来使用,是不是很方便呢?WebP与gif这两兄弟我们一般都是用来展示动图的,但是WebP也可以用来展示静态图片,WebP最大的优点就是无损压缩,体积小,但是浏览器支持太差,我们来看caniuse的数据:从图上可以看到WebP格式在苹果设备和IE上基本不支持,因此浏览器的不支持是它的硬伤,因此在对动图做展示的时候我们不得不选gif,即便它的体积很大,渲染开销也大图片优化方案图片质量压缩图片压缩应该是图片优化时最常用的方案,因为很简单,只需要将图片上传到tinypng或者智图这类的在线压缩图片平台,对图片进行压缩,就可以较小图片质量雪碧图雪碧图经常用来将多个小图标和成一张图片,然后将合成的图片当作背景图片是使用,这样可以减少图片的网络请求,使用之前可能需要请求10个网络小图标,而使用之后请求一个就可以搞定,我个人通常使用gopng这个网站在线生成,还可以自动生成对应的css代码base64将一个图片地址进行base64编码后会得到一串字符串,将这个字符直接放到img的src属性上,你会发现浏览器是可以识别这一串字符的,不需要发送网络请求直接解析,这样就可以达到减少网络请求的目的,但是base64编码后的图片质量比原图图片质量要大,因此也只会在一些质量较小的图标类图片上面使用,否则得不偿失,常见使用base64编码的方案就是webpack的url-loader,举个例子:module.exports = { module: { rules: [ { test: /.(png|jpg|gif)$/, use: [ { loader: ‘url-loader’, options: { limit: 8192 } } ] } ] }}上面的这个配置就是把8k一下的通过url-loader进行base64编码,转换成一串DataUrlcss替换简单图标这个优化方案应该都懂,其实就是在写代码之前先考虑一下设计稿里面的哪些内容是可以通过代码来实现的,能通过代码实现的尽量用代码实现,同时实现的时候多考虑绘制性能,能使用css3做GPU硬件加速的就尽量使用css3属性,这些都能减少图片使用而且不影响渲染性能响应式图片加载什么是响应式图片加载?其实就是在不同分辨率的设备上显示不同尺寸的图片,避免资源的浪费,常用的方法就是css3的媒体查询(media query),来看个例子:@media screen and (max-width: 375px) { img { background-image: url(‘phone.png’); }}@media screen and (max-width: 768px) { img { background-image: url(’tablet.png’); }}懒加载图片懒加载的目的就是为加快页面加载速度而做的,为了不让图片一次全部加载出来,通过将图片地址存放在一个img标签的属性上,当图片被滚动到页面上时,在将src属性替换成图片地址来达到懒加载的效果webpack图片优化图片压缩webpack也可以对图片进行压缩操作,通过image-webpack-loader可以对输出的图片进行指定质量的压缩,来看具体例子:{test: /.(png|jpg|gif|svg)$/,use: [ ‘file-loader’, { loader: ‘image-webpack-loader’, options: { bypassOnDebug: true, mozjpeg: { progressive: true, quality: 65 }, optipng: { enabled: false, }, pngquant: { quality: ‘65-90’, speed: 4 }, gifsicle: { interlaced: false, }, // the webp option will enable WEBP webp: { enabled: false, }, limit: 1, name: ‘[name].[ext]?[hash]’ } }]}上面的配置指定了各个格式的图片的压缩质量,并且通过hash编码重新命名输出合成雪碧图webpack的webpack-spritesmith插件提供了自动合成雪碧图的功能并且可以自动生成对应的央视文件,非常方便,来看一个具体的例子:const SpritesmithPlugin = require(‘webpack-spritesmith’)new SpritesmithPlugin({ src: { cwd: path.resolve(__dirname, ‘src/asserts’), glob: ‘*.png’ }, target: { image: path.resolve(__dirname, ‘src/spritesmith-generated/sprite.png’), css: path.resolve(__dirname, ‘src/spritesmith-generated/sprite.css’) }, apiOptions: { cssImageRef: “src/sprite.png” }})通过上面配置就能将asserts目录下的所有png文件合成雪碧图,并且输出到对应目录,同时还可以生成对应的样式文件,样式文件的语法会根据你配置的样式文件的后缀动态生成,比如这里我们配置的是sprite.css,生成的文件内容就是css语法:.icon-checkout { background-image: url(src/sprite.png); background-position: -96px -56px; width: 34px; height: 32px;}.icon-clock { background-image: url(src/sprite.png); background-position: -96px 0px; width: 56px; height: 56px;}.icon-close { background-image: url(src/sprite.png); background-position: 0px 0px; width: 96px; height: 96px;}如果将配置中的sprite.css改成sprite.scss那么生成语法就是scss的语法:@mixin sprite-width($sprite) { width: nth($sprite, 5);}@mixin sprite-height($sprite) { height: nth($sprite, 6);}@mixin sprite-position($sprite) { $sprite-offset-x: nth($sprite, 3); $sprite-offset-y: nth($sprite, 4); background-position: $sprite-offset-x $sprite-offset-y;}@mixin sprite-image($sprite) { $sprite-image: nth($sprite, 9); background-image: url(#{$sprite-image});}@mixin sprite($sprite) { @include sprite-image($sprite); @include sprite-position($sprite); @include sprite-width($sprite); @include sprite-height($sprite);}@mixin sprites($sprites) { @each $sprite in $sprites { $sprite-name: nth($sprite, 10); .#{$sprite-name} { @include sprite($sprite); } }}这样就可以根据你项目中使用的样式语言去生成所需要的语法,是不是很方便呢?总结这篇文章简单介绍网页开发中的各个图片格式的优缺和一些常用的图片优化,希望这篇文章对大家以后在做图片优化时能有所帮助。如果有错误或不严谨的地方,欢迎批评指正,如果喜欢,欢迎点赞收藏 ...

December 22, 2018 · 1 min · jiezi

webpack自动用svg生成iconfont字体图标,支持热重载

之前生成iconfont字体图标,是用的https://icomoon.io/app/ 或者是阿里的https://www.iconfont.cn/ ,将UI给的svg图导入来生成。但是一直有个问题,假如需要再次加入几个图标时,又需要重新搞一遍,很麻烦。而使用svg-sprite-loader的方式,也不是很方便,而且在body下插入一个超大的svg标签,影响性能不说,看着这么乱的代码真是挺难受。。而且有些UI库比使用字体图标会比较便利。另外字体文件特别是woff也比svg要小很多。于是最近抽时间参考开源代码搞了个webpack插件,自动用svg生成iconfont字体图标,支持热更新。使用这个插件后,开发时在src/iconfont/svgs目录下,修改或添加、删除svg文件时,就可以自动生成字体图标(支持ttf,woff2,woff,eot,svg)及配套从css样式、html预览了;同时热更新立即可以看到效果。Byebye了我滴icomoon们!源码在这:https://github.com/hzsrc/webp…感兴趣的小伙伴可以试试~下载源码安装后直接跑npm run dev就可看效果了

December 22, 2018 · 1 min · jiezi

理解vue ssr原理,自己搭建简单的ssr框架

前言大多数Vue项目要支持SSR应该是为了SEO考虑,毕竟对于WEB应用来说,搜索引擎是一个很大的流量入口。Vue SSR现在已经比较成熟了,但是如果是把一个SPA应用改造成SSR应用,成本还是有些高的,这工作量无异于重构前端。另外对前端的技术要求也是挺高的,需要对Vue比较熟悉,还要有Node.js 和 webpack 的应用经验。引入Vue是一个构建客户端应用的框架,即vue组件是在浏览器中进行渲染的。所谓服务端渲染,指的是把vue组件在服务器端渲染为组装好的HTML字符串,然后将它们直接发送到浏览器,最后需要将这些静态标记"激活"为客户端上完全可交互的应用程序。服务端渲染的优点更好的SEO,搜索引擎爬虫可以抓取渲染好的页面更快的内容到达时间(首屏加载更快),因为服务端只需要返回渲染好的HTML,这部分代码量很小的,所以用户体验更好服务端渲染的缺点首先就是开发成本比较高,比如某些声明周期钩子函数(如beforeCreate、created)能同时运行在服务端和客户端,因此第三方库要做特殊处理,才能在服务器渲染应用程序中运行。由于服务端渲染要用Nodejs做中间层,所以部署项目时,需要处于Node.js server运行环境。在高流量环境下,还要做好服务器负载和缓存策略原理解析先附上demo地址:https://github.com/wmui/vue-s…第一步:编写entry-client.js和entry-server.jsentry-client.js只在浏览器环境下执行,所以需要显示调用$mount方法,挂载DOM节点import Vue from ‘vue’;import App from ‘./App.vue’;import createStore from ‘./store/index.js’;function createApp() { const store = createStore(); const app = new Vue({ store, render: h => h(App) }); return {app, store}}const { app, store } = createApp();// 使用window.__INITIAL_STATE__中的数据替换整个state中的数据,这样服务端渲染结束后,客户端也可以自由操作state中的数据if (window.INITIAL_STATE) { store.replaceState(window.INITIAL_STATE);}app.$mount(’#app’);entry-server.js需要导出一个函数,在服务端渲染期间会被调用import Vue from ‘vue’;import App from ‘./App.vue’;import createStore from ‘./store/index.js’;export default function(context) { // context是上下文对象 const store = createStore(); let app = new Vue({ store, render: h => h(App) }); // 找到所有 asyncData 方法 let components = App.components; let asyncDataArr = []; // promise集合 for (let key in components) { if (!components.hasOwnProperty(key)) continue; let component = components[key]; if (component.asyncData) { asyncDataArr.push(component.asyncData({store})) // 把store传给asyncData } } // 所有请求并行执行 return Promise.all(asyncDataArr).then(() => { // context.state 赋值成什么,window.INITIAL_STATE 就是什么 // 这下你应该明白entry-client.js中window.__INITIAL_STATE__是哪来的了,它是在服务端渲染期间被添加进上下文的 context.state = store.state; return app; });};上面的asyncData是干嘛用的?其实,这个函数是专门请求数据用的,你可能会问请求数据为什么不在beforeCreate或者created中完成,还要专门定义一个函数?虽然beforeCreate和created在服务端也会被执行(其他周期函数只会在客户端执行),但是我们都知道请求是异步的,这就导致请求发出后,数据还没返回,渲染就已经结束了,所以无法把 Ajax 返回的数据也一并渲染出来。因此需要想个办法,等到所有数据都返回后再渲染组件asyncData需要返回一个promise,这样就可以等到所有请求都完成后再渲染组件。下面是在foo组价中使用asyncData的示例,在这里完成数据的请求export default { asyncData: function({store}) { return store.dispatch(‘GET_ARTICLE’) // 返回promise }, computed: { article() { return this.$store.state.article } }}第二步:配置webpackwebpack配置比较简单,但是也需要针对client和server端单独配置webpack.client.conf.js显然是用来打包客户端应用的module.exports = merge(base, { entry: { client: path.join(__dirname, ‘../entry-client.js’) }});webpack.server.conf.js用来打包服务端应用,这里需要指定node环境module.exports = merge(base, { target: ’node’, // 指定是node环境 entry: { server: path.join(__dirname, ‘../entry-server.js’) }, output: { filename: ‘[name].js’, // server.js libraryTarget: ‘commonjs2’ // 必须按照 commonjs规范打包才能被服务器调用。 }, plugins: [ new HtmlWebpackPlugin({ template: path.join(__dirname, ‘../index.ssr.html’), filename: ‘index.ssr.html’, files: { js: ‘client.js’ }, // client.js需要在html中引入 excludeChunks: [‘server’] // server.js只在服务端执行,所以不能打包到html中 }) ]});第三步:启动服务打包完成后就可以启动服务了,在start.js中我们需要把server.js加载进来,然后通过renderToString方法把渲染好的html返回给浏览器const bundle = fs.readFileSync(path.resolve(__dirname, ‘dist/server.js’), ‘utf-8’);const renderer = require(‘vue-server-renderer’).createBundleRenderer(bundle, { template: fs.readFileSync(path.resolve(__dirname, ‘dist/index.ssr.html’), ‘utf-8’) // 服务端渲染数据});server.get(’*’, (req, res) => { renderer.renderToString((err, html) => { // console.log(html) if (err) { console.error(err); res.status(500).end(‘服务器内部错误’); return; } res.end(html); })});效果图demo已经上传到github: http://github.com/wmui/vue-ss…结语个人实践Vue SSR已有一段时间,发现要想搭建一套完整的 SSR 服务框架还是很有挑战的,或许 Nuxt 是一个不错的选择,对 Nuxt 感兴趣的朋友可以参考我的一个开源小作品Essay以上,感谢阅读! ...

December 22, 2018 · 2 min · jiezi

构建多页面应用——模板

因为大多数人都比较喜欢,或者说倾向于用js操作现有的html代码块,而不喜欢用js来生成html代码块,之后再来操作它。很明显的一点儿就是前者清晰明了,后者不是那么直观。因此在开发中,我们会接触到模板后者模板引擎这样概念。我们比较常见的就是*.html模板,Java开发中的*.jsp,php开发中的*.php,还有用于node.js的*.ejs 和 .jade(以及它的最新版本.pug)。这里,着重说一下html和pug。如何使用html-webpack-plugin的模板和注意事项html-webpack-plugin 支持为生成的页面指定模板,我们可以直接使用配置项为template,那么这个指定的html模板应该如何操作,或者说应该怎么操作,才能达到灵活多变的特性。使用webapcK做多页面应用的构建,我们当然是希望它能够实现构建单页面应用那样的模块化处理。我们从构建建多页面应用知道了构建多页面应用,可以实现js代码的模块化,从构建多页面应用——单个页面的处理,知道了构建多页面应用可以实现css代码的模块化。那么,构建多页面应用能实现html代码的模块化吗?当然可以。在上一篇文章中,有一个title不能注入到生成的页面的问题,但是html-webpack-plugin插件可以解析<%= htmlWebpackPlugin.options.title %>这样的语法,它内部可以写js语法,同样它也解决了title不能注入到生成的页面的问题,但它有一个限制条件就是只能使用在被当作模板的html文件中,其它的代码块无法使用,所以关于html代码块的模块化,我们可以在模板文件上下下功夫。使用过jQuery的同学都知道,我们可以使用$(’#container’).load(’./pages/partial.html’)的方法引入一个html代码块。而在webapck中,我们可以使用require(’./pages/commons/header.html’)的语法引入一个html代码块,但是webapck使用require引入的是一个文件流,不能和html模板文件相融合。所以这里要引入html-loader来做处理,它可以将流文件转化为字符串,这样就可以在html模板文件中使用了。基于html-webpack-plugin的模板的实际操作首先,本文中的构建多页面应用的项目目录图:├── src │ ├── common // 公用的模块 │ │ ├── a.js // 引用了a.css,模块header.css,container.css, footer.css │ │ ├── b.js // 引用了b.css │ │ ├── c.js // 引用了c.css │ │ ├── d.js ├── pages // html代码块 │ ├── template.html // 模板文件 │ ├── commons │ │ ├── header.html │ │ ├── footer.html │ │ ├── container.html ├── assets // 静态资源 │ ├── 19224132.jpg // 用来做页面图标 │ ├── css │ │ ├── a.css │ │ ├── b.css │ │ ├── c.css ├── assets // 静态资源 │ ├── 19224132.jpg // 用来做页面图标 │ ├── css │ │ ├── a.css │ │ ├── b.css │ │ ├── c.css │ │ ├── main.css │ │ ├── abutus.css │ │ ├── footer.css │ │ ├── container.css │ │ ├── header.css │ ├── uttils // 工具 │ │ ├── load.js // 工具代码load.js │ ├── index.js // 主模块index.js (包含a.js, b.js, c.js, d.js),引用了main.css │ ├── aboutUs.js // 主模块aboutus.js (包含a.js, b.js),引用了main.css, aboutus.css │ ├── contactUs.js // 主模块contactus.js (包含a.js, c.js),引用了main.css ├── webpack.config.js // css js 和图片资源 ├── package.json ├── yarn.lock新增了pages目录,它里面包含了html-webpack-plugin所需的模板文件和组成模板的各个模块文件。根据上文的分析,以及讲述需要,我们定义了一个公用的模板文件template.html,代码如下:<!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><%= htmlWebpackPlugin.options.title %></title></head><body> <!– header –> <!– <%= require(‘html-loader!./commons/header.html’) %> –> <%= require(’./commons/header.html’) %> <!– container –> <!– <%= require(‘html-loader!./commons/container.html’) %> –> <%= require(’./commons/container.html’) %> <!– footer –> <!– <%= require(‘html-loader!./commons/footer.html’) %> –> <%= require(’./commons/footer.html’) %></body></html>注:这里可能有的人会有个疑惑。因为熟悉构建单页面应用的人都将html-loader放到webapck.config.js文件中,这样可以一次配置,终身受用,如果你也像构建单页面应用那样操作的话,你需要小心再小心,因为你不加控制的话,html-loader 也会处理template.html,再加之<%= htmlWebpackPlugin.options.title %>是html-webpack-plugin插件的独有语法,这样在webapck编译的过程中,因为html-loader先执行,所以会将它转化为字符串,而html-webpack-plugin后执行,所以,这样的情况下,webpack生成的html页面不是你所需要的。解决方法也很简单你,只需要使用include选项即可(在webapck中使用include和exclude可以是loader的处理更精确,加开快编译的速度)。当然,根据世界开发需要,我们可根据页面的不同设计效果和组成页面的不同模块来定义不同的html模板。而对于哪些组成html模板的html模块,我们可以像写普通的html代码块那样,如:header.html,它的代码如下:<div class=“header”> <ul> <li><a href=“index.html”>首页</a></li> <li><a href=“aboutus.html”>关于我们</a></li> <li><a href=“contactus.html”>联系我们</a></li> </ul></div>如果你要对生成的html页面压缩,可以使用html-webpack-plugin的minify选项。这样,构建页面应用的js、css、html代码的模块化就都实现了。源代码可参考webpack4.x multi-page文章到这里,基本算是完成了这一篇文章的目的。但是如果你想对html代码进行更加细粒度的处理,可以考虑ejs或者pug。正如本文开始说的那样,下面就只简单的介绍pug的使用。用pug对html代码进行细粒度的操作这里,需要使用到的是pug-loader,它的作用和html-loader类似,只不过它有自己的语法,要经过从pug的语法到html的转换过程。但是它有更灵活的语法,可以让我们的页面代码更简洁。有些内容只用语言可能太空洞,还是用代码还解释。首先,模板的代码:doctype htmlhtml(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= htmlWebpackPlugin.options.title body // header include ./commons/header.pug // container include ./commons/container.pug // footer include ./commons/footer.pug注:因为这里使用了pug的语法,所以要使用pug依赖包,它实现的是pug语法到html的转换,而pug-loader只是起到了一个加载解析的作用,所以使用前,我们要执行如下的安装命令:yarn add -D pug pug-loader因为pug不仅可以使用包含,还可以使用集成,扩展,迭代和混合的特性,可以让我们随心所欲的对html实现模块化,所以深受开发者的喜爱。它们的具体使用可参考pug 官方文档。具体示例可参考webpack3.x multi-page的源代码。本篇文章要介绍的内容,到这里是真的结束了。当然,还有很多东西没有说完,如果需要可关注后续的文章。构建多页面应用系列文章webpack 构建多页面应用——初探构建多页面应用——单个页面的处理构建多页面应用——模板 ...

December 21, 2018 · 2 min · jiezi

vue-router 启用 history 模式下的开发及非根目录部署

vue-router 的 history 模式是个提高颜值的好东西,没有了 hash 的路由看起来清爽许多。开发的时候,如果我们使用 devServer 来启动服务,由于一般不共用端口,我们一般不存在非根目录的问题。而刷新后 404 的问题可以借助 historyApiFallback 来解决。 但当我们项目对外开放时,往往无法在域名根目录下提供服务,这个时候资源的访问路径与开发时的根目录就有了区别。首先,我们通过 webpack 来配置一下项目中所有资源的基础路径,让这份代码在开发和生产环境中都可以正确找到资源。// config/index.jsmodule.exports = { dev: { … // 开发环境根目录 - 服务根目录 - 绝对路径 assetsPublicPath: ‘/’ … }, build: { … // 生产环境根目录 - 服务器访问路径 - 绝对路径 assetsPublicPath: ‘/test/project1/’ … }}// build/webpack.common.conf.jsconst config = require(’../config’)module.exports = { output: { publicPath: process.env.NODE_ENV === ‘production’ ? config.build.assetsPublicPath : config.dev.assetsPublicPath }}// build/webpack.dev.conf.jsconst common = require(’./webpack.common’)module.exports = merge(common, { devServer: { historyApiFallback: true }}然后在提供服务的服务器配置中做如下配置(以 nginx 为例):location /test/project1 { alias …/project1; // 项目的真实路径 index index.html; try_files $uri $uri/ /test/project1/index.html;}try_files 会按顺序检查参数中的资源是否存在,并返回第一个找到的资源,如果都没有找到,它会让 nginx 内部重定向到会后一个参数。对了,所以它的的作用是解决刷新 404 的问题。这里值得注意的是 try_files 的参数是绝对路径。至此,你开启 history 模式的项目就可以顺利的跑在任何路径了。欢迎大家点评指正点个赞~ wink原文链接 -《vue-router 启用 history 模式下的开发及非根目录部署》 ...

December 21, 2018 · 1 min · jiezi

vue 撸后台笔记一

前言本文是以 花裤衩 大佬的 vue-element-admin 项目为模板、结合公司需求开发的后台管理系统的学习笔记。原项目地址:vue-element-admin参考文章:手摸手用 vue 撸后台系列安装与配置新建 vue-cli 项目,相关安装及配置不多做介绍,有需要可自行搜索。接着是安装项目依赖。基本依赖库:Vue-Router Vue.js 官方的路由管理器Axios 基于promise 的 HTTP 库Element-UI 一套为开发者、设计师和产品经理准备的基于 Vue2.0 的桌面端组件库Vuex 一个专为 Vue.js 应用程序开发的状态管理模式扩展依赖库:node-sass css 扩展语言normalize.css 为默认的 HTML 元素样式上提供跨浏览器的高度一致性js-cookie 一款轻量级的 js 操作 cookie 的插件i18n Vue.js 的国际化插件,它可以轻松地将一些本地化特性集成到 Vue 中driver.js 一款轻量级、无需依赖但功能强大的原生 JavaScript,兼容所有主流浏览器,可帮助你将用户的注意力集中在页面上NProgress 细长的全站进度条SVG sprite loader 用于根据导入的 svg 文件自动生成 symbol 标签并插入 htmlSortable 一款轻量级的拖放排序列表的 js 插件ECharts 一款功能强大的图表和可视化库screenfull 一款全屏插件项目结构├── build // 构建相关├── config // 配置相关├── disk // 打包文件├── node_modules // 依赖项├── src // 源代码│ ├── api // 所有请求│ ├── assets // 主题 字体等静态资源│ ├── components // 全局公用组件│ ├── directive // 全局指令│ ├── waves // 水波纹指令│ ├── icons // 项目所有 svg icons│ ├── lang // 国际化 language│ ├── mock // 项目mock 模拟数据│ ├── roter // 路由│ ├── store // 全局 store管理│ ├── styles // 全局样式│ ├── utils // 全局公用方法│ ├── views // views 所有页面│ ├── account // 账户管理│ ├── court // 法院管理│ ├── dashboard // 功能主页│ ├── device // 设备管理│ ├── errorPage // 错误页面│ ├── layout // 整体布局│ ├── login // 登录页面│ ├── redirect // 重定向页面│ ├── statistics // 数据统计页面│ ├── versions // 版本管理页面│ ├── writs // 文书管理页面│ ├── App.vue // 入口页面│ ├── errorLog.js // 错误日志│ ├── main.js // 入口文件 加载组件 初始化等│ ├── permission.js // 权限管理├── static // 第三方不打包资源├── .babelrc // babel-loader 配置├── .eslintrc.js // eslint 配置项├── .gitignore // git 忽略项├── favicon.ico // favicon 图标├── index.html // html 模板├── package.json // 依赖项目录├── README.MD // 说明文档简单讲下 src 文件夹api 与 views根据项目的业务划分 views 页面展示部分,并将 api 接口请求与 views 一一对应,有利于迭代更新与后期维护。components将全局公用的模块与组件存放在 components 文件夹中,页面级的的组件建议还是放在各自的 views 文件夹下。store在 index 入口文件引入 modules 对象,独立封装各个模块状态。axios在 axios 配置档设置基础 URL,根据环境变量动态切换 api,需要在 config/dev.env.js 文件中配置接口路径。lang将中英文语言包各自封装并在入口 index.js 配置导入在 main.js 使用 i18n。 ...

December 21, 2018 · 2 min · jiezi

我把自己的经历做了提炼,起草了一个项目:抽丝剥茧的学前端之React篇

三四句话总结全文我今天终于可以自信地说我React入门了!我把这几个月学会的知识点总结在了一个项目里,我将它命名为:抽丝剥茧的学前端之React篇github仓库地址是:[https://github.com/Bedivere-Sun/write_react_project_from_scratch][1]欢迎各位前辈前来指正我理解上的问题,以免误导他人。也希望能够帮助到和我一样爱钻牛角尖的朋友,减少他们的痛苦。 啰嗦的正文大家好,这是我来segmentFault以来第一次鼓起勇气发表自己的原创文章,而且也是第一次推荐自己起草的项目:抽丝剥茧的学前端之React篇。希望多少能够帮助到和我一样或相似在React入门的道路上还在受苦的朋友们。仓库地址https://github.com/Bedivere-S…本项目的目的文档不能直观的把知识都展现出来,因此我将代码和文档都放进来,比照学习通过版本迭代的方式将React尽可能做到理解透彻,最终形成一个没有迷雾的知识库每个大的版本更新都将此前的旧版本打包,以此形成“阶段性的章节”,以免让新人一下接触太多导致混乱为每一个代码文件做注释,详细说明为什么这么写,这是什么原理,以及我自己学习过程中对这块内容的理解第一版的主要知识点介绍webpack的配置方法告诉你为什么要用这些模块,怎么用介绍babel的一些基本内容介绍html-webpack-plugin的内容,具体写法,所有选型的意义简历了一个简单的页面,学会通过引入资源的方式将其内容展示出来心历路程我今年10月报名学习腾讯的React课程,但是一直处于懵8的状况。月中开始到11月以来我基本都是有如没头苍蝇一般四处碰壁。我和学习群里的一些同学还吵过架,原因很简单,我提出一个问题,总会有人说“你没有前端基础,应该看看基础的内容”……我吵回去的理由也很简单很傻:“你说我没基础,那你倒是说说我到底哪里没基础啊,前端基础那么多的领域,我应该看哪个部分的基础啊?”这个问题困扰了我半个多月。后来学习群里有几位前辈给我推荐了一些书,还有的前辈真的认真倾听了我的问题,指出我要学习什么基础知识。11月开始,我列了一个todo list,画了张思维导图,基本上学习计划就非常清晰了。于是开始了各种请教,然后开始猛啃官方文档全文。中英文文档其实还是有很多差距的,一些单词翻译不出来,还有的部分干脆直接被省略了……所以我就只好对照着看,然后还是在群里和各位前辈们钻牛角尖……可以说我11月积累了很多知识,我把这些知识点都收集了起来,然后再根据自己读文档的印象……我突然就觉得自己明白了很多,写简单的项目也没有什么疑虑了。然后到了12月8号,我起草了这个项目,把自己学到的知识点做了一个提炼,希望能够帮助到和我一样曾经感到迷茫的小伙伴们。2018年12月19日

December 19, 2018 · 1 min · jiezi

webpack入门学习手记(三)

本人微信公众号:前端修炼之路,欢迎关注。距离上一次更新这个系列,过去了两天。最近实在是有点忙,没有挤出时间整理。感觉日更还真是困难????以下是正文。管理资源如果看过之前的系列文章,应该会有一个学习项目webpackStudy,可以从文章下方找到之前的链接。官网给出的示例都是在一个项目中的html页面、package.json和webpack.config.js中进行修改。我为了保留每一小节的代码,并没有按照官网给出的方案处理,而是重新新建的配置文件。可以通过腾讯云开发者平台查看源码。webpack有两大特色:动态打包。在webpack中,每个模块都会声明所引用的依赖,这样就避免了打包没有使用到的模块。另外通过配置,可以避免重复打包相同的引用,提高打包效率。强大的loader。通过loader,webpack可以引入任何其他的非JavaScript文件。例如,加载css、图片、字体、JSON、XML等。加载css首先安装两个loader:style-loader、css-loader。执行如下命令:npm install –save-dev style-loader css-loader注意建议使用淘宝 NPM 镜像然后在webpack.config.js中添加使用loader的规则。webpack.config.jsconst path = require(‘path’);module.exports = { entry: ‘./src/index.js’, output: { filename: ‘bundle.js’, path: path.resolve(__dirname, ‘dist’) }, module: { rules: [{ test: /.css$/, use: [‘style-loader’, ‘css-loader’] }] }};添加module.rules表示要使用的loader规则。test属性使用正则表达式匹配任何的.css文件。use属性通过一个数组,表示匹配到的文件使用哪些需要加载的loader,这里就是style-loader和css-loader。接下来在项目中添加一个style.css文件,并修改下index.js。project|- /src+ |- style.css |- index.js |- /node_modulessrc/style.css.hello { color: red;}src/index.jsimport _ from ’lodash’;import ‘./style.css’;function component() { let element = document.createElement(‘div’); // Lodash, currently included via a script, is required for this line to work element.innerHTML = _.join([‘Hello’, ‘webpack’], ’ ‘); element.classList.add(‘hello’); return element;}document.body.appendChild(component());在index.js文件中,通过import将style.css文件引入。在style.css中添加了一个hello样式。index.js文件中,直接使用了这个css样式。也就是说,在js文件中,直接使用了css代码。感觉非常酷然后执行打包命令,看看有什么变化npm run build此时用浏览器打开index.html,会发现之前的Hello webpack变成了红色。请注意,之前我们并没有在index.html中引入任何的css样式。用浏览器检查一下页面,就会知道webpack是怎么做到的了。说明webpack将css代码自动添加到head标签中了,非常的智能化~加载图片接下来尝试下加载图片和在css中引用背景图片。这时我们要使用file-loader。npm install –save-dev file-loader在webpack.config.js中添加一段使用loader的配置。webpack.config.jsmodule: { rules: [{ test: /.css$/, use: [‘style-loader’, ‘css-loader’] }, { test: /.(png|svg|jpg|gif)$/, use: [‘file-loader’] }] }然后在项目中添加一张图片。project|- /src+ |- icon.jpg |- style.css |- index.js |- /node_modules最后,添加引用图片和使用背景图片代码。src/index.jsimport _ from ’lodash’;import ‘./style.css’;import Icon from ‘./icon.jpg’;function component() { let element = document.createElement(‘div’); // Lodash, currently included via a script, is required for this line to work element.innerHTML = _.join([‘Hello’, ‘webpack’], ’ ‘); element.classList.add(‘hello’); // Add the image to our existing div. var myIcon = new Image(); myIcon.src = Icon; element.appendChild(myIcon); return element;}document.body.appendChild(component());src/style.css.hello { color: red; background: url(./icon.jpg);}重新执行打包命令npm run build,然后打开index.html文件,会发现已经能显示添加的图片和设置的背景图了。这里还有一个小细节是,如果查看页面会发现,图片名称已经被修改为类似14a53ef4a1ced4a4a6f7161f51c6870e.jpg这样的名字了。说明webpack处理了添加的图片,并重新命名了。 关于更多的图片压缩和优化,以后再继续整理。加载字体加载字体与加载图片和css没有什么区别。我找了一个ttf格式的字体来学习这个过程。首先告诉webpack字体文件使用file-loader进行加载。webpack.config.jsmodule: { rules: [{ test: /.css$/, use: [‘style-loader’, ‘css-loader’] }, { test: /.(png|svg|jpg|gif)$/, use: [‘file-loader’] }, { test: /.(woff|woff2|eot|ttf|otf)$/, use: [‘file-loader’] }] }然后项目中引入字体:project|- /src+ |- my-font.ttf |- icon.png |- style.css |- index.js |- /node_modules最后使用字体。src/style.css@font-face{ font-family: ‘myFont’; src: url(’./my-font.ttf’) format(’ttf’); font-weight: 600; font-style: normal;}.hello { color: red; font-family: ‘myFont’; background: url(./icon.jpg);}重新打包npm run build,打开index.html文件,然后查看页面,会发现字体已经使用上了。与图片相同,字体文件也被wepack重命名了。加载数据webpack可以加载任何类型的数据,例如JSON, CSV, TSV, 和XML。webpack默认内置了JSON加载数据。照着官网的例子一步步来。npm install –save-dev csv-loader xml-loaderwebpack.config.jsmodule: { rules: [{ test: /.css$/, use: [‘style-loader’, ‘css-loader’] }, { test: /.(png|svg|jpg|gif)$/, use: [‘file-loader’] }, { test: /.(woff|woff2|eot|ttf|otf)$/, use: [‘file-loader’] }, { test: /.(csv|tsv)$/, use: [‘csv-loader’] }, { test: /.xml$/, use: [‘xml-loader’] }] }添加一个测试用的xml数据:project|- /src+ |- data.xml |- my-font.woff |- my-font.woff2 |- icon.png |- style.css |- index.js |- /node_modulesdata.xml<?xml version=“1.0” encoding=“UTF-8”?><note> <to>Mary</to> <from>John</from> <heading>Reminder</heading> <body>Call Cindy on Tuesday</body></note>src/index.jsimport _ from ’lodash’;import ‘./style.css’;import Icon from ‘./icon.jpg’;import Data from ‘./data.xml’;function component() { let element = document.createElement(‘div’); // Lodash, currently included via a script, is required for this line to work element.innerHTML = _.join([‘Hello’, ‘webpack’], ’ ‘); element.classList.add(‘hello’); // Add the image to our existing div. var myIcon = new Image(); myIcon.src = Icon; console.log(Data); element.appendChild(myIcon); return element;}document.body.appendChild(component());目前执行到这里一切看起来都很容易。创建一个data.xml文件,然后加一些假数据,最后在index.js中打印这个数据。但是当我执行打包命令npm run build时,一切都不那么的和谐了。出现了如下的错误:build error> webpackStudy@1.0.0 build /Users/yyy/Documents/work/workspace/webpackStudy> webpack –config webpack.config.js/Users/yyy/Documents/work/workspace/webpackStudy/node_modules/.3.1.2@webpack-cli/bin/cli.js:244 throw err; ^Error: Cannot find module ‘@webassemblyjs/helper-code-frame’ at Function.Module._resolveFilename (internal/modules/cjs/loader.js:603:15) at Function.Module._load (internal/modules/cjs/loader.js:529:25) at Module.require (internal/modules/cjs/loader.js:658:17) at require (/Users/yyy/Documents/work/workspace/webpackStudy/node_modules/.2.0.2@v8-compile-cache/v8-compile-cache.js:159:20) at Object.<anonymous> (/Users/yyy/Documents/work/workspace/webpackStudy/node_modules/.1.7.11@@webassemblyjs/wast-parser/lib/grammar.js:8:24) at Module._compile (/Users/yyy/Documents/work/workspace/webpackStudy/node_modules/.2.0.2@v8-compile-cache/v8-compile-cache.js:178:30) at Object.Module._extensions..js (internal/modules/cjs/loader.js:733:10) at Module.load (internal/modules/cjs/loader.js:620:32) at tryModuleLoad (internal/modules/cjs/loader.js:560:12) at Function.Module._load (internal/modules/cjs/loader.js:552:3) at Module.require (internal/modules/cjs/loader.js:658:17) at require (/Users/yyy/Documents/work/workspace/webpackStudy/node_modules/.2.0.2@v8-compile-cache/v8-compile-cache.js:159:20) at Object.<anonymous> (/Users/yyy/Documents/work/workspace/webpackStudy/node_modules/.1.7.11@@webassemblyjs/wast-parser/lib/index.js:11:38) at Module._compile (/Users/yyy/Documents/work/workspace/webpackStudy/node_modules/.2.0.2@v8-compile-cache/v8-compile-cache.js:178:30) at Object.Module._extensions..js (internal/modules/cjs/loader.js:733:10) at Module.load (internal/modules/cjs/loader.js:620:32) at tryModuleLoad (internal/modules/cjs/loader.js:560:12)这就很奇怪了,意思是说找不到一个模块。经过一番苦苦查找,终于知道了原因。需要将webpack4版本 降到webpack3版本。我找到的版本是3.11.0。我将wepack卸载掉,然后安装这个版本之后,再次打包,就成功了。从上图的中的打印数据中可以发现,我们的xml文件已经被解析成了json格式的数据。说明:我将本小节代码托管到了腾讯云开发者平台,如果需要查看这节内容,请查找Asset Management目录即可。以上就是指南手册中的Asset Management部分。总结一下主要内容:加载CSS加载图片加载字体加载数据下一篇笔记整理webpack官方文档的指南手册剩余部分,敬请关注。(待续)相关文章webpack入门学习手记(一)webpack入门学习手记(二)webpack入门学习手记(三) ...

December 19, 2018 · 2 min · jiezi

这才是官方的tapable中文文档

起因搜索引擎搜索tapable中文文档,你会看见各种翻译,点进去一看,确实是官方的文档翻译过来的,但是webpack的文档确实还有很多需要改进的地方,既然是开源的为什么不去github上的tapable库看呢,一看,确实,比webpack文档上的描述得清楚得多.tapable 是一个类似于nodejs 的EventEmitter 的库, 主要是控制钩子函数的发布与订阅,控制着webpack的插件系.webpack的本质就是一系列的插件运行.TapableTapable库 提供了很多的钩子类, 这些类可以为插件创建钩子const { SyncHook, SyncBailHook, SyncWaterfallHook, SyncLoopHook, AsyncParallelHook, AsyncParallelBailHook, AsyncSeriesHook, AsyncSeriesBailHook, AsyncSeriesWaterfallHook } = require(“tapable”);安装npm install –save tapable使用所有的钩子构造函数,都接受一个可选的参数,(这个参数最好是数组,不是tapable内部也把他变成数组),这是一个参数的字符串名字列表const hook = new SyncHook([“arg1”, “arg2”, “arg3”]);最好的实践就是把所有的钩子暴露在一个类的hooks属性里面:class Car { constructor() { this.hooks = { accelerate: new SyncHook([“newSpeed”]), brake: new SyncHook(), calculateRoutes: new AsyncParallelHook([“source”, “target”, “routesList”]) }; } /* … /}其他开发者现在可以这样用这些钩子const myCar = new Car();// Use the tap method to add a consument// 使用tap 方法添加一个消费者,(生产者消费者模式)myCar.hooks.brake.tap(“WarningLampPlugin”, () => warningLamp.on());这需要你传一个名字去标记这个插件:你可以接收参数myCar.hooks.accelerate.tap(“LoggerPlugin”, newSpeed => console.log(Accelerating to ${newSpeed}));在同步钩子中, tap 是唯一的绑定方法,异步钩子通常支持异步插件// promise: 绑定promise钩子的APImyCar.hooks.calculateRoutes.tapPromise(“GoogleMapsPlugin”, (source, target, routesList) => { // return a promise return google.maps.findRoute(source, target).then(route => { routesList.add(route); });});// tapAsync:绑定异步钩子的APImyCar.hooks.calculateRoutes.tapAsync(“BingMapsPlugin”, (source, target, routesList, callback) => { bing.findRoute(source, target, (err, route) => { if(err) return callback(err); routesList.add(route); // call the callback callback(); });});// You can still use sync plugins// tap: 绑定同步钩子的APImyCar.hooks.calculateRoutes.tap(“CachedRoutesPlugin”, (source, target, routesList) => { const cachedRoute = cache.get(source, target); if(cachedRoute) routesList.add(cachedRoute);})类需要调用被声明的那些钩子class Car { / … / setSpeed(newSpeed) { // call(xx) 传参调用同步钩子的API this.hooks.accelerate.call(newSpeed); } useNavigationSystemPromise(source, target) { const routesList = new List(); // 调用promise钩子(钩子返回一个promise)的API return this.hooks.calculateRoutes.promise(source, target, routesList).then(() => { return routesList.getRoutes(); }); } useNavigationSystemAsync(source, target, callback) { const routesList = new List(); // 调用异步钩子API this.hooks.calculateRoutes.callAsync(source, target, routesList, err => { if(err) return callback(err); callback(null, routesList.getRoutes()); }); }}钩子会用最有效率的方式去编译(构建)一个运行你的插件的方法,他生成的代码依赖于一下几点:你注册的插件的个数.你注册插件的类型.你使用的调用方法(call, promise, async) // 其实这个类型已经包括了钩子参数的个数 // 就是你new xxxHook([‘ooo’]) 传入的参数是否应用了拦截器(拦截器下面有讲)这些确定了尽可能快的执行.钩子类型每一个钩子都可以tap 一个或者多个函数, 他们如果运行,取决于他们的钩子类型基本的钩子, (钩子类名没有waterfall, Bail, 或者 Loop 的 ), 这个钩子只会简单的调用每个tap进去的函数Waterfall, 一个waterfall 钩子,也会调用每个tap进去的函数,不同的是,他会从每一个函数传一个返回的值到下一个函数Bail, Bail 钩子允许更早的退出,当任何一个tap进去的函数,返回任何值, bail类会停止执行其他的函数执行.(类似 Promise.reace())Loop, TODO(我…. 这里也没描述,应该是写文档得时候 还没想好这个要怎么写,我尝试看他代码去补全,不过可能需要点时间.)此外,钩子可以是同步的,也可以是异步的,Sync, AsyncSeries 和 AsyncParallel 钩子就反应了这个问题Sync, 一个同步钩子只能tap同步函数, 不然会报错.AsyncSeries, 一个 async-series 钩子 可以tap 同步钩子, 基于回调的钩子(我估计是类似chunk的东西)和一个基于promise的钩子(使用myHook.tap(), myHook.tapAsync() 和 myHook.tapPromise().).他会按顺序的调用每个方法.AsyncParallel, 一个 async-parallel 钩子跟上面的 async-series 一样 不同的是他会把异步钩子并行执行(并行执行就是把异步钩子全部一起开启,不按顺序执行).拦截器(interception)所有钩子都提供额外的拦截器API// 注册一个拦截器myCar.hooks.calculateRoutes.intercept({ call: (source, target, routesList) => { console.log(“Starting to calculate routes”); }, register: (tapInfo) => { // tapInfo = { type: “promise”, name: “GoogleMapsPlugin”, fn: … } console.log(${tapInfo.name} is doing its job); return tapInfo; // may return a new tapInfo object }})call:(…args) => void当你的钩子触发之前,(就是call()之前),就会触发这个函数,你可以访问钩子的参数.多个钩子执行一次tap: (tap: Tap) => void 每个钩子执行之前(多个钩子执行多个),就会触发这个函数loop:(…args) => void 这个会为你的每一个循环钩子(LoopHook, 就是类型到Loop的)触发,具体什么时候没说register:(tap: Tap) => Tap | undefined 每添加一个Tap都会触发 你interceptor上的register,你下一个拦截器的register 函数得到的参数 取决于你上一个register返回的值,所以你最好返回一个 tap 钩子.Context(上下文)插件和拦截器都可以选择加入一个可选的 context对象, 这个可以被用于传递随意的值到队列中的插件和拦截器.myCar.hooks.accelerate.intercept({ context: true, tap: (context, tapInfo) => { // tapInfo = { type: “sync”, name: “NoisePlugin”, fn: … } console.log(${tapInfo.name} is doing it's job); // context starts as an empty object if at least one plugin uses context: true. // 如果最少有一个插件使用 context 那么context 一开始是一个空的对象 // If no plugins use context: true, then context is undefined // 如过tap进去的插件没有使用context 的 那么内部的context 一开始就是undefined if (context) { // Arbitrary properties can be added to context, which plugins can then access. // 任意属性都可以添加到context, 插件可以访问到这些属性 context.hasMuffler = true; } }});myCar.hooks.accelerate.tap({ name: “NoisePlugin”, context: true}, (context, newSpeed) => { if (context && context.hasMuffler) { console.log(“Silence…”); } else { console.log(“Vroom!”); }});HookMap一个 HookMap是一个Hooks映射的帮助类const keyedHook = new HookMap(key => new SyncHook([“arg”]))keyedHook.tap(“some-key”, “MyPlugin”, (arg) => { / … / });keyedHook.tapAsync(“some-key”, “MyPlugin”, (arg, callback) => { / … / });keyedHook.tapPromise(“some-key”, “MyPlugin”, (arg) => { / … / });const hook = keyedHook.get(“some-key”);if(hook !== undefined) { hook.callAsync(“arg”, err => { / … */ });}钩子映射接口(HookMap interface)Public(权限公开的):interface Hook { tap: (name: string | Tap, fn: (context?, …args) => Result) => void, tapAsync: (name: string | Tap, fn: (context?, …args, callback: (err, result: Result) => void) => void) => void, tapPromise: (name: string | Tap, fn: (context?, …args) => Promise<Result>) => void, intercept: (interceptor: HookInterceptor) => void}interface HookInterceptor { call: (context?, …args) => void, loop: (context?, …args) => void, tap: (context?, tap: Tap) => void, register: (tap: Tap) => Tap, context: boolean}interface HookMap { for: (key: any) => Hook, tap: (key: any, name: string | Tap, fn: (context?, …args) => Result) => void, tapAsync: (key: any, name: string | Tap, fn: (context?, …args, callback: (err, result: Result) => void) => void) => void, tapPromise: (key: any, name: string | Tap, fn: (context?, …args) => Promise<Result>) => void, intercept: (interceptor: HookMapInterceptor) => void}interface HookMapInterceptor { factory: (key: any, hook: Hook) => Hook}interface Tap { name: string, type: string fn: Function, stage: number, context: boolean}Protected(保护的权限),只用于类包含的(里面的)钩子interface Hook { isUsed: () => boolean, call: (…args) => Result, promise: (…args) => Promise<Result>, callAsync: (…args, callback: (err, result: Result) => void) => void,}interface HookMap { get: (key: any) => Hook | undefined, for: (key: any) => Hook}MultiHook把其他的Hook 重定向(转化)成为一个 MultiHookconst { MultiHook } = require(“tapable”);this.hooks.allHooks = new MultiHook([this.hooks.hookA, this.hooks.hookB]);OK 所有的内容我都已翻译完成.其中有很多不是直译,这样写下来感觉就是按照原文的脉络重新写了一遍….,应该能更清楚明白,要不是怕丢脸我就给个原创了,哈哈.之后, 我还会写一篇完整的原创解析,直击源码,搞定tapable, 完全了解webpack插件系统(webpack本来就是一个插件的事件流), 好久没写原创了. 我自己也很期待. ...

December 18, 2018 · 4 min · jiezi

不满足于只会使用系列: tapable

全方位的,零死角的,分析tapable源码上一遍博文中,我们谈到了tapable的用法,现在我们来深入一下tap究竟是怎么运行的, 怎么处理,控制 tap 进去的钩子函数,拦截器又是怎么运行的.俺们先从同步函数说起,异步就留给你们做练习把(哈哈哈);tap这里有一个例子let SyncHook = require(’./lib/SyncHook.js’)let h1 = new SyncHook([‘options’]);h1.tap(‘A’, function (arg) { console.log(‘A’,arg); return ‘b’; // 除非你在拦截器上的 register 上调用这个函数,不然这个返回值你拿不到.})h1.tap(‘B’, function () { console.log(‘b’)})h1.tap(‘C’, function () { console.log(‘c’)})h1.tap(‘D’, function () { console.log(’d’)})h1.intercept({ call: (…args) => { console.log(…args, ‘————-intercept call’); }, // register: (tap) => { console.log(tap, ‘——————intercept register’); return tap; }, loop: (…args) => { console.log(…args, ‘————-intercept loop’) }, tap: (tap) => { console.log(tap, ‘——————-intercept tap’) }})h1.call(6);new SyncHook([‘synchook’])首先先创建一个同步钩子对象,那这一步会干什么呢?这一步会先执行超类Hook的初始化工作// 初始化constructor(args) { // 参数必须是数组 if (!Array.isArray(args)) args = []; // 把数组参数赋值给 _args 内部属性, new 的时候传进来的一系列参数. this._args = args; // 绑定taps,应该是事件 this.taps = []; // 拦截器数组 this.interceptors = []; // 暴露出去用于调用同步钩子的函数 this.call = this._call; // 暴露出去的用于调用异步promise函数 this.promise = this._promise; // 暴露出去的用于调用异步钩子函数 this.callAsync = this._callAsync; // 用于生存调用函数的时候,保存钩子数组的变量,现在暂时先不管. this._x = undefined;}第二部 .tap()现在我们来看看调用了tap() 方法后发生了什么tap(options, fn) { // 下面是一些参数的限制,第一个参数必须是字符串或者是带name属性的对象, // 用于标明钩子,并把钩子和名字都整合到 options 对象里面 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”); // 注册拦截器 options = this._runRegisterInterceptors(options); // 插入钩子 this._insert(options);}现在我们来看看如何注册拦截器_runRegisterInterceptors(options) { // 现在这个参数应该是这个样子的{fn: function…, type: sync,name: ‘A’ }// 遍历拦截器,有就应用,没有就把配置返还回去for (const interceptor of this.interceptors) { if (interceptor.register) { // 把选项传入拦截器注册,从这里可以看出,拦截器的register 可以返回一个新的options选项,并且替换掉原来的options选项,也就是说可以在执行了一次register之后 改变你当初 tap 进去的方法 const newOptions = interceptor.register(options); if (newOptions !== undefined) options = newOptions; }}return options;}注意: 这里执行的register拦截器是有顺序问题的, 这个执行在tap()里面,也就是说,你这个拦截器要在调用tap(),之前就调用 intercept()添加的.那拦截器是怎么添加进去的呢,来看下intercept()intercept(interceptor) { // 重置所有的 调用 方法,在教程中我们提到了 编译出来的调用方法依赖的其中一点就是 拦截器. 所有每添加一个拦截器都要重置一次调用方法,在下一次编译的时候,重新生成. this._resetCompilation(); // 保存拦截器 而且是复制一份,保留原本的引用 this.interceptors.push(Object.assign({}, interceptor)); // 运行所有的拦截器的register函数并且把 taps[i],(tap对象) 传进去. // 在intercept 的时候也会遍历执行一次当前所有的taps,把他们作为参数调用拦截器的register,并且把返回的 tap对象(tap对象就是指 tap函数里面把fn和name这些信息整合起来的那个对象) 替换了原来的 tap对象,所以register最好返回一个tap, 在例子中我返回了原来的tap, 但是其实最好返回一个全新的tap if (interceptor.register) { for (let i = 0; i < this.taps.length; i++) this.taps[i] = interceptor.register(this.taps[i]); }}注意: 也就是在调用tap() 之后再传入的拦截器,会在传入的时候就为每一个tap 调用register方法现在我们来看看_insert_insert(item) { // 重置资源,因为每一个插件都会有一个新的Compilation this._resetCompilation(); // 顺序标记, 这里联合 test 包里的Hook.js一起使用 // 看源码不懂,可以看他的测试代码,就知道他写的是什么目的. // 从测试代码可以看到,这个 {before}是插件的名字. let before; // before 可以是单个字符串插件名称,也可以是一个字符串数组插件. if (typeof item.before === “string”) { before = new Set([item.before]); } else if (Array.isArray(item.before)) { before = new Set(item.before); } // 阶段 // 从测试代码可以知道这个也是一个控制顺序的属性,值越小,执行得就越在前面 // 而且优先级低于 before let stage = 0; if (typeof item.stage === “number”) stage = item.stage; let i = this.taps.length; // 遍历所有tap了的函数,然后根据 stage 和 before 进行重新排序. // 假设现在tap了 两个钩子 A B B 的配置是 {name: ‘B’, before: ‘A’} while (i > 0) {// i = 1, taps = [A] i–;// i = 0 首先– 是因为要从最后一个开始 const x = this.taps[i];// x = A this.taps[i + 1] = x;// i = 0, taps[1] = A i+1 把当前元素往后移位,把位置让出来 const xStage = x.stage || 0;// xStage = 0 if (before) {// 如果有这个属性就会进入这个判断 if (before.has(x.name)) {// 如果before 有x.name 就会把这个插件名称从before这个列表里删除,代表这个钩子位置已经在当前的钩子之前 before.delete(x.name); continue;// 如果before还有元素,继续循环,执行上面的操作 } if (before.size > 0) { continue;// 如果before还有元素,那就一直循环,直到第一位. } } if (xStage > stage) {// 如果stage比当前钩子的stage大,继续往前挪 continue; } i++; break; } this.taps[i] = item;// 把挪出来的位置插入传进来的钩子}这其实就是一个排序算法, 根据before, stage 的值来排序,也就是说你可以这样tap进来一个插件h1.tap({ name: ‘B’, before: ‘A’ }, () => { console.log(‘i am B’) })发布订阅模式发布订阅模式是一个在前后端都盛行的一个模式,前端的promise,事件,等等都基于发布订阅模式,其实tapable 也是一种发布订阅模式,上面的tap 只是订阅了钩子函数,我们还需要发布他,接下来我们谈谈h1.call(),跟紧了,这里面才是重点.我们可以在初始化中看到this.call = 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 }});结果很明显,这个函数是由createCompileDelegate(),这个函数返回的,依赖于,函数的名字以及钩子的类型.createCompileDelegate(name, type)function createCompileDelegate(name, type) { return function lazyCompileHook(…args) { // 子类调用时,this默认绑定到子类 // (不明白的可以了解js this指向,一个函数的this指向调用他的对象,没有就是全局,除非使用call apply bind 等改变指向) // 在我们的例子中,这个 this 是 SyncHook this[name] = this._createCall(type); // 用args 去调用Call return thisname; };}在上面的注释上可以加到,他通过闭包保存了name跟type的值,在我们这个例子中,这里就是this.call = this._createCall(‘sync’);然后把我们外部调用call(666) 时 传入的参数给到他编译生成的方法中.注意,在我们这个例子当中我在call的时候并没有传入参数.这时候这个call方法的重点就在_createCall方法里面了._createCall()_createCall(type) { // 传递一个整合了各个依赖条件的对象给子类的compile方法 return this.compile({ taps: this.taps, interceptors: this.interceptors, args: this._args, type: type });}从一开始,我们就在Hook.js上分析,我们来看看Hook上的compilecompile(options) { throw new Error(“Abstract: should be overriden”);}清晰明了,这个方法一定要子类复写,不然报错,上面的_createCompileDelegate的注释也写得很清楚,在当前的上下文中,this指向的是,子类,在我们这个例子中就是SyncHook来看看SyncHook 的compilecompile(options) { // 现在options 是由Hook里面 传到这里的 // options // { // taps: this.taps, tap对象数组 // interceptors: this.interceptors, 拦截器数组 // args: this._args, // type: type // } // 对应回教程中的编译出来的调用函数依赖于的那几项看看,是不是这些,钩子的个数,new SyncHook([‘arg’])的参数个数,拦截器的个数,钩子的类型. factory.setup(this, options); return factory.create(options);}好吧 现在来看看setup, 咦? factory 怎么来的,原来const factory = new SyncHookCodeFactory();是new 出来的现在来看看SyncHookCodeFactory 的父类 HookCodeFactoryconstructor(config) { // 这个config作用暂定.因为我看了这个文件,没看到有引用的地方, // 应该是其他子类有引用到 this.config = config; // 这两个不难懂, 往下看就知道了 this.options = undefined; this._args = undefined;}现在可以来看一下setup了setup(instance, options) { // 这里的instance 是syncHook 实例, 其实就是把tap进来的钩子数组给到钩子的_x属性里. instance._x = options.taps.map(t => t.fn);}OK, 到create了这个create有点长, 看仔细了,我们现在分析同步的部分.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;}到了这个方法,一切我们都一目了然了(看content的参数), 在我们的例子中他是通过动态的生成一个call方法,根据的条件有,钩子是否有context 属性(这个是根据header的代码才能知道), 钩子的个数, 钩子的类型,钩子的参数,钩子的拦截器个数.注意,这上面有关于 fn这个变量的函数,返回的都是字符串,不是函数不是方法,是返回可以转化成代码执行的字符串,思维要转变过来.现在我们来看看header()header() { let code = “”; // this.needContext() 判断taps[i] 是否 有context 属性, 任意一个tap有 都会返回 true if (this.needContext()) { // 如果有context 属性, 那_context这个变量就是一个空的对象. code += “var _context = {};\n”; } else { // 否则 就是undefined code += “var _context;\n”; } // 在setup()中 把所有tap对象的钩子 都给到了 instance ,这里的this 就是setup 中的instance _x 就是钩子对象数组 code += “var _x = this._x;\n”; // 如果有拦截器,在我们的例子中,就有一个拦截器 if (this.options.interceptors.length > 0) { // 保存taps 数组到_taps变量, 保存拦截器数组 到变量_interceptors code += “var _taps = this.taps;\n”; code += “var _interceptors = this.interceptors;\n”; } // 如果没有拦截器, 这里也不会执行.一个拦截器只会生成一次call // 在我们的例子中,就有一个拦截器,就有call for (let i = 0; i < this.options.interceptors.length; i++) { const interceptor = this.options.interceptors[i]; if (interceptor.call) { // getInterceptor 返回的 是字符串 是 _interceptors[i] // 后面的before 因为我们的拦截器没有context 所以返回的是undefined 所以后面没有跟一个空对象 code += ${this.getInterceptor(i)}.call(${this.args({ before: interceptor.context ? "_context" : undefined })});\n; } } return code; // 注意 header 返回的不是代码,是可以转化成代码的字符串(这个时候并没有执行). /** * 此时call函数应该为: * “use strict”; * function (options) { * var _context; * var _x = this._x; * var _taps = this.taps; * var _interterceptors = this.interceptors; * // 我们只有一个拦截器所以下面的只会生成一个 * _interceptors[0].call(options); } /}现在到我们的this.content()了,仔细一看,this.content()方法并不在HookCodeFactory上,很明显这个content是由子类来实现的,往回看看这个create是由谁调用的?没错,是SuncHookCodeFactory的石料理,我们来看看SyncHook.js上的SyncHookCodeFactory实现的content在看这个content实现之前,先来回顾一下父类的create()给他传了什么参数.this.content({ onError: err => throw ${err};\n, onResult: result => return ${result};\n, onDone: () => “”, rethrowIfPossible: true})注意了,这上面不是抛出错误,不是返回值. 这里面的回调执行了以后返回的是一个字符串,不要搞混了代码与可以转化成代码的字符串.content({ onError, onResult, onDone, rethrowIfPossible }) { return this.callTapsSeries({ // 可以在这改变onError 但是这里的 i 并没有用到,这是什么操作… // 注意这里并没有传入onResult onError: (i, err) => onError(err), onDone, // 这个默认为true rethrowIfPossible });}这个函数返回什么取决于this.callTapSeries(), 那接下来我们来看看这个函数(这层层嵌套,其实也是有可斟酌的地方.看源码不仅要看实现,代码的组织也是很重要的编码能力)刚才函数的头部已经出来了,头部做了初始化的操作,与生成执行拦截器代码.content很明显,要开始生成执行我们的tap对象的代码了(如果不然,我们的tap进来的函数在哪里执行呢? 滑稽:).callTapsSeries({ onError, onResult, onDone, rethrowIfPossible }) { // 如果 taps 钩子处理完毕,执行onDone,或者一个tap都没有 onDone() 返回的是一个字符串.看上面的回顾就知道了. if (this.options.taps.length === 0) return onDone(); // 如果由异步钩子,把第一个异步钩子的下标,如果没有这个返回的是-1 const firstAsync = this.options.taps.findIndex(t => t.type !== “sync”); // 定义一个函数 接受一个 number 类型的参数, i 应该是taps的index // 从这个函数的命名来看,这个函数应该会递归的执行 // 我们先开最后的return语句,发现第一个传进来的参数是0 const next = i => { // 如果 大于等于钩子函数数组长度, 返回并执行onDone回调,就是tap对象都处理完了 // 跳出递归的条件 if (i >= this.options.taps.length) { return onDone(); } // 这个方法就是递归的关键,看见没,逐渐往上遍历 // 注意这里只是定义了方法,并没有执行 const done = () => next(i + 1); // 传入一个值 如果是false 就执行onDone true 返回一个 "" // 字面意思,是否跳过done 应该是增加一个跳出递归的条件 const doneBreak = skipDone => { if (skipDone) return “”; return onDone(); }; // 这里就是处理单个taps对象的关键,传入一个下标,和一系列回调. return this.callTap(i, { // 调用的onError 是 (i, err) => onError(err) , 后面这个onError(err)是 () => throw ${err} // 目前 i done doneBreak 都没有用到 onError: error => onError(i, error, done, doneBreak), // 这里onResult 同步钩子的情况下在外部是没有传进来的,刚才也提到了 // 这里onResult是 undefined onResult: onResult && (result => { return onResult(i, result, done, doneBreak); }), // 没有onResult 一定要有一个onDone 所以这里就是一个默认的完成回调 // 这里的done 执行的是next(i+1), 也就是迭代的处理完所有的taps onDone: !onResult && (() => {return done();}), // rethrowIfPossible 默认是 true 也就是返回后面的 // 因为没有异步函数 firstAsync = -1. // 所以返回的是 -1 < 0,也就是true, 这个可以判断当前的是否是异步的tap对象 // 这里挺妙的 如果是 false 那么当前的钩子类型就不是sync,可能是promise或者是async // 具体作用要看callTaps()如何使用这个. rethrowIfPossible: rethrowIfPossible && (firstAsync < 0 || i < firstAsync) }); }; return next(0);}参数搞明白了,现在,我们可以进入callTap() 了.callTap挺长的,因为他也分了3种类型分别处理,像create()一样./ tapIndex 下标 * onError:() => onError(i,err,done,skipdone) , * onReslt: undefined * onDone: () => {return: done()} //开启递归的钥匙 * rethrowIfPossible: false 说明当前的钩子不是sync的. */callTap(tapIndex, { onError, onResult, onDone, rethrowIfPossible }) { let code = “”; // hasTapCached 是否有tap的缓存, 这个要看看他是怎么做的缓存了 let hasTapCached = false; // 这里还是拦截器的用法,如果有就执行拦截器的tap函数 for (let i = 0; i < this.options.interceptors.length; i++) { const interceptor = this.options.interceptors[i]; if (interceptor.tap) { if (!hasTapCached) { // 这里getTap返回的是 _taps[0] _taps[1]… 的字符串 // 这里生成的代码就是 var _tap0 = _taps[0] // 注意: _taps 变量我们在 header 那里已经生成了 code += var _tap${tapIndex} = ${this.getTap(tapIndex)};\n; // 可以看到这个变量的作用就是,如果有多个拦截器.这里也只会执行一次. // 注意这句获取_taps 对象的下标用的是tapIndex,在一次循环中,这个tapIndex不会变 // 就是说如果这里执行多次,就会生成多个重复代码,不稳定,也影响性能. // 但是你又要判断拦截器有没有tap才可以执行,或许有更好的写法 // 如果你能想到,那么你就是webpack的贡献者了.不过这样写,似乎也没什么不好. hasTapCached = true; } // 这里很明显跟上面的getTap 一样 返回的都是字符串 // 我就直接把这里的code 分析出来了,注意 这里还是在循坏中. // code += _interceptor[0].tap(_tap0); // 由于我们的拦截器没有context,所以没传_context进来. // 可以看到这里是调用拦截器的tap方法然后传入tap0对象的地方 code += ${this.getInterceptor(i)}.tap(${ interceptor.context ? "_context, " : "" }_tap${tapIndex});\n; } } // 跑出了循坏 // 这里的getTapFn 返回的也是字符串 _x[0] // callTap用到的这些全部在header() 那里生成了,忘记的回头看一下. // 这里的code就是: var _fn0 = _x[0] code += var _fn${tapIndex} = ${this.getTapFn(tapIndex)};\n; const tap = this.options.taps[tapIndex]; // 开始处理tap 对象 switch (tap.type) { case “sync”: // 全是同步的时候, 这里不执行, 如果有异步函数,那么恭喜,有可能会报错.所以他加了个 try…catch if (!rethrowIfPossible) { code += var _hasError${tapIndex} = false;\n; code += “try {\n”; } // 前面分析了 同步的时候 onResult 是 undefined // 我们也分析一下如果走这里会怎样 // var _result0 = _fn0(option) // 可以看到是调用tap 进来的钩子并且接收参数 if (onResult) { code += var _result${tapIndex} = _fn${tapIndex}(${this.args({ before: tap.context ? "_context" : undefined })});\n; } else { // 所以会走这里 // _fn0(options) 额… 我日 有就接受一下结果 code += _fn${tapIndex}(${this.args({ before: tap.context ? "_context" : undefined })});\n; } // 把 catch 补上,在这个例子中没有 if (!rethrowIfPossible) { code += “} catch(_err) {\n”; code += _hasError${tapIndex} = true;\n; code += onError("_err"); code += “}\n”; code += if(!_hasError${tapIndex}) {\n; } // 有onResult 就把结果给传递出去. 目前没有 if (onResult) { code += onResult(_result${tapIndex}); } // 有onDone() 就调用他开始递归,还记得上面的next(i+1) 吗? if (onDone) { code += onDone(); } // 这里是不上上面的if的大括号,在这个例子中没有,所以这里也不执行 if (!rethrowIfPossible) { code += “}\n”; } // 同步情况下, 这里最终的代码就是 // var _tap0 = _taps[0]; // _interceptors[0].tap(_tap0); // var _fn0 = _x[0]; // _fn0(options); // 可以看到,这里会递归下去 // 因为我们tap了4个钩子 // 所以这里会从复4次 // 最终长这样 // 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); // …… break; case “async”: let cbCode = “”; if (onResult) cbCode += (_err${tapIndex}, _result${tapIndex}) =&gt; {\n; else cbCode += _err${tapIndex} =&gt; {\n; cbCode += if(_err${tapIndex}) {\n; cbCode += onError(_err${tapIndex}); cbCode += “} else {\n”; if (onResult) { cbCode += onResult(_result${tapIndex}); } if (onDone) { cbCode += onDone(); } cbCode += “}\n”; cbCode += “}”; code += _fn${tapIndex}(${this.args({ before: tap.context ? "_context" : undefined, after: cbCode })});\n; break; case “promise”: code += var _hasResult${tapIndex} = false;\n; code += var _promise${tapIndex} = _fn${tapIndex}(${this.args({ before: tap.context ? "_context" : undefined })});\n; code += if (!_promise${tapIndex} || !_promise${tapIndex}.then)\n; code += throw new Error('Tap function (tapPromise) did not return promise (returned ' + _promise${tapIndex} + ')');\n; code += _promise${tapIndex}.then(_result${tapIndex} =&gt; {\n; code += _hasResult${tapIndex} = true;\n; if (onResult) { code += onResult(_result${tapIndex}); } if (onDone) { code += onDone(); } code += }, _err${tapIndex} =&gt; {\n; code += if(_hasResult${tapIndex}) throw _err${tapIndex};\n; code += onError(_err${tapIndex}); code += “});\n”; break; } return code;}好了, 到了这里 我们可以把compile 出来的call 方法输出出来了"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);}到了这里可以知道,我们的例子中h1.call()其实调用的就是这个方法.到此我们可以说是知道了这个库的百分之80了.不知道大家有没有发现,这个生成的函数的参数列表是从哪里来的呢?往回翻到create()方法里面调用的this.args()你就会看见,没错就是this._args. 这个东西在哪里初始化呢? 翻一下就知道,这是在Hook.js这个类里面初始化的,也就是说你h1 = new xxxHook([‘options’]) 的时候传入的数组有几个值,那么你h1.call({name: ‘haha’}) 就能传几个值.看教程的时候他说,这里传入的是一个参数名字的字符串列表,那时候我就纳闷,什么鬼,我传入的不是值吗,怎么就变成了参数名称,现在完全掌握….好了,最简单的SyncHook 已经搞掂,但是一看tapable内部核心使用的钩子却不是他,而是SyncBailHook,在教程中我们已经知道,bail是只要有一个钩子执行完了,并且返回一个值,那么其他的钩子就不执行.我们来看看他是怎么实现的.从刚才我们弄明白的synchook,我们知道了他的套路,其实生成的函数的header()都是一样的,这次我们直接来看看bailhook实现的content()方法content({ onError, onResult, onDone, rethrowIfPossible }) { return this.callTapsSeries({ onError: (i, err) => onError(err), // 看回callTapsSeries 就知道这里传入的next 是 done onResult: (i, result, next) => if(${result} !== undefined) {\n${onResult( result )};\n} else {\n${next()}}\n, onDone, rethrowIfPossible });}看出来了哪里不一样吗? 是的bailhook的 callTapsSeries传了onResult属性,我们来看看他这个onResult是啥黑科技父类传的onResult默认是 (result) => ‘return ${result}’,那么他这里返回的就是:// 下面返回的是字符串,if (xxx !== undefined) { // 这里说明,只要有返回值(因为不返回默认是undefined),就会立即return; return result;} else { // next(); 这里返回的是一个字符串(因为要生成字符串代码) // 我在上面的注释中提到了 next 是 done 就是那个开启递归的门 // 所以如果tap 一直没返回值, 这里就会一直 if…else.. 的嵌套下去 }回头想想,我们刚刚是不是分析了capTap(),如果我们传了onResult 会怎样? 如果你还记得就知道,如果有传了onResult这个回调,他就会接收这个返回值.并且会调用这个回调把result传出去.而且还要注意的是,onDone在callTap()的时候是处理过的,我在贴出来一次.onDone:!onResult && (() => {return done();})也就是说如果我传了onResult 那么这个onDone就是一个false.所以递归的门现在从sync的onDone,变到syncBail的onResult了好,现在带着这些变化去看this.capTap(),你就能推出现在这个 call 函数会变成这样.“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]; var _result0 = _fn0(options); if (_result0 !== undefined) { // 这里说明,只要有返回值(因为不返回默认是undefined),就会立即return; return _result0 } else { var _tap1 = _taps[1]; _interceptors[1].tap(_tap1); var _fn1 = _x[1]; var _result1 = _fn1(options); if (_result1 !== undefined) { return _result1 } else { var _tap2 = _taps[2]; _interceptors[2].tap(_tap2); var _fn2 = _x[2]; var _result2 = _fn2(options); if (_result2 !== undefined) { return _result2 } else { var _tap3 = _taps[3]; _interceptors[3].tap(_tap3); var _fn3 = _x[3]; _fn3(options); } } }到如今,tapable库 已经删除了 tapable.js文件(可能做了一些整合,更细分了),只留下了钩子文件.但不影响功能,webpack 里的compile compilation 等一众重要插件,都是基于 tapable库中的这些钩子.现在我们require(’tapable’)得到的对象是这样的:{ SyncHook: function(…){}, SyncBailHook: function(…){}, …}到此,关于tapable的大部分我都解剖了一遍,还有其他类型的hook 如果你们愿意,相信你们去研究一下,也能够游刃有余.那个,写得有些随性,可能会让你们觉得模糊,但是…我真尽力了,这篇改了几遍,历时一个星期…,不懂就在那个评论区问我.我看到会回复的.共勉.后记:本来以为会很难,但是越往下深入的时候发现,大神之所以成为大神,不是他的代码写得牛,是他的思维牛,没有看不懂的代码,只有跟不上的思路,要看懂他如何把call 函数组织出来不难,难的是,他居然能想到这样来生成函数,还可以考虑到,拦截器钩子,和context 属性,以及他的 onResult onDone 回调的判断,架构的设计,等等,一步接一步.先膜拜吧… ...

December 18, 2018 · 10 min · jiezi

构建多页面应用——单个页面的处理

在看这篇文章之前,需要你对构建多页面应用有一定的基础认识,如果没有的话,可以先参考这篇文章webpack 构建多页面应用。多页面应用是由一个个独立的页面组成。因此,细粒度的处理一个个单页面是构建单页面框架之后的一个重要实现。因为所涵盖的知识点较碎,所以就不按照页面的位置结合组成元素来讲,如:head, body, script等。这里主要介绍head。因为script操作其实就是上一篇文章中已经介绍过的js操作,而body因为内容较多,需要另起一篇文章。页面的头部在上一篇文章中,我们讲述了如何用html-webpack-plugin 生成一个html文件,其中使用了两个配置项chunks,filename,前者指代页面所要引入的js模块,也就是我们常见的html页面中的<script src="…"></script>形式,后者指代文件的名字。那么,在这一部分,要说的就是如何给不同的页面配置生成不同的页面<head>…</head>。我们都知道页面头部包括title、link/style、meta、script 这四部分组成,尤其前三者居多。当然,在web前端开发中js很强大,我们可以用js直接控制,在不同页面的入口js文件中写相应的js代码。这种方法虽然可行,但维护起来比较麻烦,当你修改的时候,你需要查找一个个页面。相对来讲,使用html-webpack-plugin提供的配置项,会使你的开发工作变得简单起来。html-webpack-plugin 插件的配置项title 选项可以为页面指定名字,meta 选项可以为页面指定html文档关联信息,如:描述,作者等,favicon 可以为页面添加一个小图标。 修改 webpack.config.js,代码如下:…nnew HtmlWebapckPlugin({ /* inital page / filename: ‘index.html’, chunks: [‘index’], / page head */ title: ‘index’, meta: { ‘description’: ‘这是首页’, ‘keywords’: ‘webpack, multi-page, 首页’, ‘author’: ‘https://github.com/lvzhenbang/ }, favicon: ‘./assets/19884132.jpg’})…这样头部常用的三个元素我们已经解决了两个。那么接下来就是解决link这个元素的。注:有一个比较特殊的就是html页面图标<link rel=“shortcut icon” href=“19884132.jpg”> ,我们使用 html-webpack-plugin 插件的 favicon 选项已经解决。link 和 style 部分的处理这两个元素常常被用来处理样式。link 处理外部样式,style 处理内联样式。注:很多人会误解,或曲解,这里的样式处理是这样的:在定义的页面入口文件,或者页面入口文件引用的文件中,引入css文件,webapck会将这些样式以内联的形式或者link的形式注入到生成的html页面中。这样我们的应用的目录结构就变成如下这样(本片文章使用如下的目录结构,它也介绍了各个js文件对css文件的引用):├── src │ ├── common // 公用的模块 │ │ ├── a.js // 引用了a.css │ │ ├── b.js // 引用了b.css │ │ ├── c.js // 引用了c.css │ │ ├── d.js ├── assets // 静态资源 │ ├── 19224132.jpg // 用来做页面图标 │ ├── css │ │ ├── a.css │ │ ├── b.css │ │ ├── c.css │ │ ├── main.css │ │ ├── abutus.css │ ├── uttils // 工具 │ │ ├── load.js // 工具代码load.js │ ├── index.js // 主模块index.js (包含a.js, b.js, c.js, d.js),引用了main.css │ ├── aboutUs.js // 主模块aboutus.js (包含a.js, b.js),引用了main.css, aboutus.css │ ├── contactUs.js // 主模块contactus.js (包含a.js, c.js),引用了main.css ├── webpack.config.js // css js 和图片资源 ├── package.json ├── yarn.lock处理为内联样式如果是webpack3.x 推荐使用 css-loader,style-loader,extract-text-webpack-plugin;如果是webapck4.x推荐使用的 css-loader, mini-css-extract-plugin。webpack3.x与webapck4.x都一样,修改webpack.config.js如下:…module: { rules: [ { test: /.css$/, use: [ ‘style-loader’, ‘css-loader’ ] } ]},…因为mini-css-extract-plugin是专门为webpack4.x设计的,如果webapck3.x使用它会报错。处理为外部链接(link)webpack3.x中webpack.config.js修改如下:…const ExtractTextPlugin = require(’extract-text-webapck-plugin’)… module: { rules: [ { test: /.css$/, use: ExtractTextPlugin.extract({ fallback: ‘style-loader’, use: ‘css-loader’ }) } ] }, plugins: [ … new ExtractTextPlugin({ filename: ‘[name].css’ }) ]webpack4.x中webpack.config.js修改如下:…const MiniCssExtractPlugin = require(‘mini-css-extract-plugin’)… module: { rules: [ { test: /.css$/, use: [ MiniCssExtractPlugin.loader, ‘css-loader’ ] } ] }, plugins: [ … MiniCssExtractPlugin() ],从js文件中分离出css文件,webpack3.x借助于extract-text-webpack-plugin,webpack4.x借助于mini-css-extract-plugin,前者给不同的css代码块命名需要在extract-text-webapck-plugin的示例中配置,它使用的是内置的CommonsChunkPlugin插件的拆分原则,后者不需要配置分离css代码块的名字选项,后者借助于SplitsChunkPlugin插件的拆分原则。所以,它们的分离形式与js代码块一致。webpack3.x为每个入口点生成了一个css文件,并提取了它们的公共代码生成了一个新的css文件;webapck4.x为每个入口生成了一个css文件,并提取并生成了这些文件相互之间的公共文件(它和前者不同,后者更精细化,只要是某一个或者几个文件有公共代码就提取出来,然后生成新的文件)。为什么将css文件和js文件分的这么细?是因为这样可以显著的减小首次加载页面时请求文件的大小(lazyload),但是这样做会增加HTTP的请求次数。在多页面应用的过程中,有的人喜欢将所有的css放在一个或两个文件中,而不是像本文中那样为每个页面生成一个css文件,包括它们之间的共用文件。但在多页面应用中,这样精密的细分也有其好处。相对来说,使用CommonsChunkPlugin拆分的css模块更合理些,而使用SplitsChunkPlugin拆分的css模块,则过于细化。至于如何取舍,还需要根据实际情况来定。当然,这里面还有一些小的问题需要优化,后期我会视情况来写相应的文章描述。源代码webpack3.x multi-pagewebpack4.x multi-page构建多页面应用系列文章webpack 构建多页面应用——初探构建多页面应用——单个页面的处理 ...

December 18, 2018 · 2 min · jiezi

WebPack牛刀小试

现在页面的功能和需求越来越复杂,繁复杂乱的JavaScript代码和一大堆的依赖包都需要包含在前端页面中。如果还用手动处理就有点像在现代战场上使用小米加步枪的味道了。为了减小开发的复杂度,前端社区涌现出了层出不穷的实践方案,比如TypeScript扩展语言、SCSS、LESS类的CSS预处理器,还有模块化思想等。这些新兴技术的出现一定程度上提高了我们的编程效率,然而它们还不能被浏览器直接识别。手动处理它们又无疑会显得效率不高,同时早期一些模块打包器不能完全满足目前大型项目对代码分割和静态资源无缝模块化的迫切需求,因此,WebPack应运而生了。今天,本文就向你介绍这款前端工具WebPack。一、什么是WebPack?我们先来看看官方解读:WebPack 是一个现代 JavaScript 应用程序的模块打包器(module bundler),它将你的项目作为一个整体,通过入口文件(如index.js)找到所有的依赖文件,并递归地构建一个依赖关系图(dependency graph),其中包含应用程序需要的每个模块,然后使用loaders将所有这些模块打包成少量的(通常只有一个)、浏览器可以识别的bundle,再交给由浏览器去加载。其目的就是解决现在前端越来越复杂的文件依赖问题。如下图所示:通俗的讲,WebPack通过分析你的项目结构,找到JavaScript模块以及其它的一些浏览器不能直接运行的拓展语言(Scss,TypeScript等),将其转换和打包为合适的格式供浏览器使用,并根据指定的规则实现静态资源的分类合并,减少页面请求。二、为什么要用WebPack?简单的说,使用工具当然是为了提升效率啦WebPack的以下几大优势使得它深受前端开发人员的喜爱:·模块化:它能把各种资源,如JS、coffee、样式(含less/scss)、图片等都作为模块来使用和处理,把复杂的程序简单化·方便旧代码迁移:它的脚本使用CommonJS形式来书写,并提供对AMD/CMD的支持,因此很方便的就可以把旧代码迁移·可扩展性强:WebPack有一个智能解析器,能处理几乎所有的第三方库,支持多种插件可以说,可扩展性强是WebPack的最大优势和特色了,在当今这个快速更迭的前端行业,不支持各种插件就等于是没有前景可言,WebPack支持多种多样的插件,能够满足你对各种插件的依赖。三、如何使用WebPack?首先需要将WebPack进行全局安装,在本地建立一个空的项目文件夹,如WebPack test,在文件夹目录下打开终端,输入:npm install –g WebPack如下图所示:安装完成后,你可以手动到项目目录下建立一个package.json文件,来增加项目的描述信息,如项目名、版本、入口文件等等,也可直接在终端输入命令:npm init这将会自动在根目录下创建该文件,命令会自动提示你输入项目的相关信息,你可以自行定义,也可以直接回车使用默认值,如下图所示:由于WebPack是全局安装的,我们需要将其加入项目中,作为依赖包使用,继续执行如下命令:npm install ——save-dev WebPack此时我们的项目文件中将会出现相关的依赖包node_modules.之后,你就可以创建自定义的文件目录来存放项目的相关文件啦例如,我们可以在项目的根目录下创建两个文件夹src和template,分别用来存放自己编写的js文件和WebPack打包生成的文件,然后再创建一个index.html文件放在根目录下作为展示页面,index.html的内容如下。接下来我们需要在src文件夹下实现两个js文件,作为WebPack打包的源文件,分别为root.js和node.js.两个文件的内容如下图所示。这两个文件中root.js需要依赖node.js的实现,同时我们将root.js作为WebPack打包的入口文件。这里为了便于后续文章内容的理解,先阐明WebPack的四个核心概念:入口(entry)、输出(output)、loader、插件(plugins)。入口(entry)是WebPack打包各个文件依赖关系的起点(entry point),它告诉 WebPack 从哪里开始,并根据依赖关系图确定需要打包的内容。输出(output)是告诉WebPack如何去处理这些打包文件,并且最终这些打包内容生成到哪里。Loader是WebPack中相对比较重要的一个概念,它需要识别出(identify)应该被对应的 loader 进行转换(transform)的那些文件并且转换这些文件,从而使其能够被添加到依赖图中。插件(plugins)相对于Loader而言的不同之处在于Loader 仅在每个文件的基础上执行转换,而插件(plugins) 更常用于在打包模块的 “compilation” 和 “chunk” 生命周期执行操作和自定义功能。为了方便地对项目依赖文件进行编译,WebPack允许我们通过配置文件的方式把所有与打包相关的信息放到里面,提高编译效率。那么我们要如何来写这个配置文件呢?首先我们需要在项目的根目录下创建一个名为webpack.config.js的文件,文件中配置内容如下:其中entry就是我们前面提到的入口文件,需要在配置项中告诉WebPack编译器我们要去哪里寻找项目的入口文件;output配置项则定义了编译后的打包文件的输出目录和文件名称,其输出目录由path项指定,文件名称则由filename指定。本文中以前面创建的template目录作为打包文件的输出路径。然后我们就可以执行WebPack的编译命令了,我们需要在终端进入到项目的根目录下然后执行WebPack,该命令会自动使用webpack.config.js中的配置内容。配置完成后,在根目录打开index.html文件,即可看到意料之中的结果啦~在实际项目中,可能有多种WebPack命令,这些命令往往是比较复杂的。为此,我们可以通过配置npm来代替他们,通常是“npm run +配置命令”的形式,而这些配置则是在package.json文件中的script标签中完成的,且可以设置多个不同的json对象值。其中,start命令是npm内置的特殊命令,其执行不需要加额外的run命令,直接输入npm start即可执行。由上图可知,经过script配置后,通过npm start命令可以实现与WebPack命令同样的打包结果。值得注意的是,打包之后的文件是很不容易找到对应的源文件的,这也就很不利于我们的调试工作。别着急,这些问题WebPack早就替你想到了,所以它还提供了一个Source Maps功能,来获取源文件和编译后文件的对应关系,只需要简单地配置devtool,就可以很容易地在调试时定位错误,大大提升了开发调试效率。在实际开发过程中,我们希望对代码的修改能及时自动反映在浏览器中,而不是总是手动编译运行。为此WebPack提供了一个可选的本地开发服务器,该服务器基于node.js构建,是一个单独的组件,要配置它需要提前安装该服务器作为项目的依赖,在终端运行以下命令即可完成安装。npm install ——save-dev WebPack-dev-server然后,需要对devserver进行配置,有以下四个内容:·contentBase:设置提供服务器的项目目录·port:设置浏览器的监听端口·inline:true,设置为true表示浏览器会自动监听端口并时时刷新页面·historyApiFallback:设置页面是否自动跳转在此基础上,还需要在package.json文件的script对象中加上一个json对象“server”: “WebPack-dev-server ——open”,此时,本地服务器的配置就大功告成了!尝试着在终端运行命令:npm run server以上就是WebPack的基本功能啦,用它来打包我们的项目灰常的简单千万别以为只有这点功能而小看了WebPack!在实际的项目开发过程中,我们更需要的是它强大的其他特性,比如loaders、plugins插件等,这些都可以根据自己的需要进行配置

December 17, 2018 · 1 min · jiezi

webpack 构建多页面应用

如何使用webpack构建多页面应用,这是一个我一直在想和解决的问题。网上也给出了很多的例子,很多想法。猛一看,觉得有那么点儿意思,但仔细看也就那样。使用webpack这个构建工具,可以使我们少考虑很多的问题。我们常见的单页面应用只有一个页面,它考虑问题,解决问题围绕着中心化去解决,因此很多麻烦都迎刃而解。如果你使用过vue.js,那么想必你一定用过vue-router,vuex,它们就是典型的中心化管理模式,当然还有很多,这里不一一列举了。而多页面应用,我们不能再按照中心化模式的路走了,因为行不通,这也是很多人认为多页面应用不好做,或者干脆认为webapck只能做单页面应用,而不能做多页面应用的原因。所以,我要说明的第一点儿是:不要用做单页面应用的思维来做多页面应用。单页面中的模块共享和多页面的模块共享的区别单页面的模块共享,其实是代码块在同一个页面的不同位置的重复出现;而多页面应用的代码块儿共享需要实现的不仅是同一个页面的共享,还要做到跨页面的共享。所以,第一个要解决的问题是:不同页面的代码块共享如何实现?单页面的路由管理,其实是根据用户的触发条件来实现不同的代码块的显隐;而多页面应用的路由管理则不然,它实现的是页面的跳转。所以,第二个要解决的问题是:所页面应用的导航该如何做?单页面的状态管理,很受开发者喜好。单页面是一个页面,所以页面中的数据状态的管理操作起来还算得心应手,那么多页面应用的呢,显然依靠它自身很难实现。所以,第三个要解决的问题是:多页面应用的状态管理如何做?注:这个问题问的其实有点儿傻,如果你做的是dom操作的多页面儿应用,就不用做状态管理了。如果你还是使用想vue.js这样的库,你就需要考虑要不要再用做多页面的状态管理了,因为此法儿就是为单页面应用做的,多页面儿行不通。多页面应用的探索入口(entry):webpack对入口不仅可以定义单个文件,也可以定义多个文件。熟悉当页面应用开发的对于下面的代码应该不会陌生吧?module.exports = { entry: ‘./src/index.js’, ···}我第一次接触真正的单页面应用项目使用的就是angualrjs,使用的构建工具使webapck+gulp,其中的webpack.config.js 中的看到的入口文件代码就是它。后来,接触到的是数组形式,代码如下:module.exports = { entry: [’./src/index.js’, ‘bootstrap’] ···}这样,将bootstrap和入口文件一起引用,就可以在任何一个代码块中使用boostrap。再后来,接触到的是对象形式,代码如下:module.exports = { main: ‘./src/index.js’ ···}这样做的目的是为了给输出的文件指定特定的名字。再后来,就是做多页面应用,就需要用到如下的代码:module.exports = { entry: { index: ‘./src/index.js’, aboutUs: ‘./src/aboutus.js’, contactUs: ‘./src/contactus.js’ }}为了引入第三方库,我们可以像如下这样做:module.exports = { entry: { index: [’./src/index.js’, ’loadsh’], aboutUs: ‘./src/aboutus.js’, contactUs: [’./src/contactus.js’, ’lodash’] }}webpack3.x的探索但为了共享模块代码,我们需要像下面这这样做:const CommonsChunkPlugin = require(‘webpack’).optimization.CommonsChunkPluginmodule.exports = { entry: { index: [’./src/index.js’, ‘./src/utils/load.js’, ’loadsh’], aboutUs: [’./src/aboutus.js’, ’loadsh’], contactUs: [’./src/contactus.js’,’./src/utils/load.js’, ’lodash’] }, plugins: [ new CommonsChunkPlugin({ name: “commons”, filename: “commons.js”, chunks: [“index”, “aboutUs”, “contactUs”] }) ]}这样型就会形成如下所示的项目目录结构:├── src │ ├── common // 公用的模块 │ │ ├── a.js │ │ ├── b.js │ │ ├── c.js │ │ ├── d.js │ ├── uttils // 工具 │ │ ├── load.js // 工具代码load.js │ ├── index.js // 主模块index.js (包含a.js, b.js, c.js, d.js) │ ├── aboutUs.js // 主模块aboutus.js (包含a.js, b.js) │ ├── contactUs.js // 主模块contactus.js (包含a.js, c.js) ├── webpack.config.js // css js 和图片资源 ├── package.json ├── yarn.lock但是这个内置插件的局限性比较大。正如上面所使用的那样,它只会提取chunks选项所匹配的模块共有的代码块。就如同上面代码表示的那样,它只会提取pindex, aboutUs, contactUs共有的代码块loadsh,而不会提取index, contactUs共有的代码块load.js。当然,一般的第三方库,我们也不这样使用,而是像下面这样使用:const CommonsChunkPlugin = require(‘webpack’).optimization.CommonsChunkPluginmodule.exports = { entry: { index: [’./src/index.js’, ‘./src/utils/load.js’], aboutUs: [’./src/aboutus.js’], contactUs: [’./src/contactus.js’,’./src/utils/load.js’], vendors: [’lodash’] }, externals: { commonjs: “lodash”, root: “_” }, plugins: [ new CommonsChunkPlugin({ name: “commons”, filename: “commons.js”, chunks: [“index”, “aboutUs”, “contactUs”] }) ]}对于web应用最终的目的是:匹配生成不同的html页面。这里我们要使用的就是html-webpack-plugin。首先,需要安装html-webpack-plugin:yarn add –dev html-webpack-plugin然后引入插件,并配置如下:…const HtmlWebapckPlugin = require(‘html-webpack-plugin’);… plugins: [ … new HtmlWebapckPlugin({ filename: ‘index.html’, chunks: [‘vendors’, ‘commons’, ‘index’] }), new HtmlWebapckPlugin({ filename: ‘aboutUs.html’, chunks: [‘vendors’, ‘commons’, ‘aboutUs’] }), new HtmlWebapckPlugin({ filename: ‘contactUs.html’, chunks: [‘commons’, ‘contactUs’] }) ], …这样一个基于webpack3.x的多页面框架就有了基本的样子。webpack4.x的探索而使用webpack4.x则完全不同,它移除了内置的CommonsChunkPlugin插件,引入了SplitChunksPlugin插件,这个插件满足了我们的需要,弥补了CommonsChunkPlugin的不足。如果你想要解决之前的不足,去提取index, contacUs共有的模块,操作起来会很简单。正如上面的所列举的那样,我们有三个入口点index, aboutUs, contactUs,SplitChunksPlugin 插件会首先获取这三个入口点共有的代码块,然后建立一个文件,紧接着获取每两个入口点的共有代码块,然后将每个入口点独有的代码块单独形成一个文件。如果你使用了第三方库,就像上面我们使用的loadsh,它会将第三方入口代码块单独打包为一个文件。配置文件webpack.config.js需要增加如下的代码:···optimization: { splitChunks: { chunks: ‘all’, maxInitialRequests: 20, maxAsyncRequests: 20, minSize: 40 }}···因为SplitChunksPlugin可以提取任意的入口点之间的共同代码,所以,我们就不需要使用vendors入口节点了。那么,为匹配生成不同的页面代码可以修改成如下:const HtmlWebapckPlugin = require(‘html-webpack-plugin’)··· plugins: [ new HtmlWebapckPlugin({ filename: ‘index.html’, chunks: [‘index’] }), new HtmlWebapckPlugin({ filename: ‘aboutUs.html’, chunks: [‘aboutUs’] }), new HtmlWebapckPlugin({ filename: ‘contactUs.html’, chunks: [‘contactUs’] }), ]···可以发现结果越来越接近我们所想。但是这里还是存在一个问题,第三方库loadsh因为在入口点index, aboutUs中被分别引入,但是构建的结果却输出了两个第三方库文件,这不是我们想要的。这个问题怎么解决呢,因为html-webpack-plugin插件的chunks选项,支持多入口节点,所以,我们可以再单独创建一个第三方库的入口节点vendors。配置代码修改如下:… entry: { index: [’./src/index.js’, ‘./src/utils/load.js’], aboutUs: [’./src/aboutUs.js’], contactUs: [’./src/contactUs.js’,’./src/utils/load.js’], vendors: [’loadsh’] }, … plugins: [ new HtmlWebapckPlugin({ filename: ‘index.html’, chunks: [‘index’, ‘vendors’] }), new HtmlWebapckPlugin({ filename: ‘aboutUs.html’, chunks: [‘aboutUs’, ‘vendors’] }), new HtmlWebapckPlugin({ filename: ‘contactUs.html’, chunks: [‘contactUs’] }), ],…注意:如果不同的入口点儿之间有依赖关系,如上面的index和vendors之间,因为index依赖于vendors,所以vendors要置于index之前。这篇文章,说到这里基本上已经结束了。当然,webpack多页面应用的知识点还没有讲完,这些内容会放在后续的文章中详解。源代码webpack3.x multi-pagewebpack4.x multi-page ...

December 17, 2018 · 2 min · jiezi

webpack入门学习手记(二)

本人微信公众号:前端修炼之路,欢迎关注。最近开始想要维护一个个人的公众号,初心是为了督促自己坚持做笔记,将学习的东西整理记录下来。更进一步的要求是提升动手能力、文章写作能力,和分享经验。终极目标就是为了维护一个个人的品牌形象,像提到@阮一峰、@张鑫旭、@情封、@justjavac 等等业内知名的IT技术人员一样,然后用自己的品牌形象获取一些合法的经济利益,用来改善生活。毕竟努力学习、认真工作,说到底还是为了更好的生活。鉴于以上种种原因和目的,我开通了微信公众号,并将文章更新到SegmentFault、掘金,进一步扩大传播度。目前原创文章已经写了7期。在这个过程中主要有以下几点体会:1、无法坚持原创日更的囧境。因为我的写作动机是自己的学习笔记、工作笔记的整理。也就是说,我要有不断学习和工作的过程,然后将这个过程遇到的问题和解决方法记录整理下来。这就需要强制性的养成一些习惯,并且压缩掉一些自己以前花时间做的事情。目前这几件事情做到。但是我无法处理掉临时性突发的事件,尤其是处理起来比较费时间的事情。一旦将事情解决完毕,自己已经筋疲力尽,无心无力再坚持日更了。目前想到的解决思路是,首先依然是要坚持原创日更文章,内容集中在学习笔记、工作笔记和一些反思等。然后一旦有临时性突发事件耗费自己大量时间和经历的情况发生,导致可能第二天无法更新文章,可以选择转载一些优秀的文章。目前取得了一位公众号作者@苏南 的转载权限。希望以后能获得更多的作者转载权限。2、没有文章可写的囧境。之前的几篇文章主要是自己遇到的一些问题,文章的连续性并不强。如果是想保证能日更,那就必须有新的问题出现,并且是值得用来写文章的。并不是说随便的一个问题,例如标点符号错误,语法语句错误,就能拿过来写一篇文章的。再有就是字数也不能太少,否则直接在SF上发一个笔记就好了。另外估计只有20、30的文章,是小学生写的作文吧~ 没有什么阅读的价值。所以我想到的解决思路是,做一个系列。例如现在的《webpack入门学习手记》系列。首先是我在学习webpack,然后就是webpack的各方面知识点足够我更新一段时间,最后就是文章内容足够多,也足够丰富。所以这个系列就诞生了。当这个系列更新完毕之后,也不用慌,还有ES6、Vue等等系列都可以写。另外就是一些非系列的性的文章,可以穿插到日更里面。这样公众号的内容,也就丰富起来,也不会担心没有文章、没有原创文章了。3、收到粉丝留言和打赏的喜悦。我的更新思路是,文章是先写到SF平台,利用Markdown快速写好文章,然后更新到掘金,最后更新到微信公众平台,等到第二天定时群发消息。这个过程中,我SF的声望已经突破了1.5K,掘金和SF的粉丝关注度也在不断增加。每次看到又有新的粉丝和点赞收藏,就有继续写下去动力。因为掘金和SF不同于微信,都是互相未曾谋面之人,仅仅是因为文章的好坏而关注和收藏的。相比于SF和掘金,公众号的粉丝大部分是我的亲朋好友。他们被我强制性的要求关注我的公众号。其实他们中有很多人,根本就不知道我写的是什么,但是依然关注了公众号。非常感谢他们的支持~ 相比于粉丝数的增长,最开心的还是收到了真金白银的打赏。只不过目前打赏一直没有进到我的账户,不知道微信是怎么弄的。其实这几期里面,给我打赏最多就是我爸妈了,我每写的一篇文章他们都会下方默默的打赏我1元钱~ 可怜天下父母心啊~ 不同于别的公众号打赏,我设置的金额是1元、2元、5元,小额打赏。因为我觉得打赏金额过高,反而会吓到一些朋友。能给我打赏一元,就已经非常开心了。综上所述,无论遇到的问题有多难,也会义无反顾的坚持下去。以下是正文。安装上一篇webpack入门学习手记(一),主要是介绍了webpack的核心概念,是整个学习过程的基础知识。接下来按照官方文档给出的指南手册依次进行。另外我会根据个人理解和操作过程,对文档内容有一些修改,如有出入请参考原文。要想使用webpack,前提是必须安装Node.js,另外请保证使用最新的稳定版本。否则Node的版本过低,会导致意想不到的问题出现。官方手册中给出了两种安装方式,一种是本地安装(Local Installation)和全局安装(Global Installation)。在黄色的警告框处,明显提示说,不建议全局安装,因为会将webpack锁定到指定的版本,另外也会在不同的webpack版本的项目中,构建失败。所以我们直接使用本地安装即可。首先安装最新版本的webpack:npm install –save-dev webpack因为使用的webpack v4 以上的版本,所以再安装cli工具:npm install –save-dev webpack-cli注意:其实我在安装的时候,使用的不是npm命令,而是cnpm。因为众所周知的原因,国内下载安装会比较慢甚至卡顿,所以使用了淘宝镜像,可以自行百度下具体说明~ 以下文章中提到的所有npm命令地方,都请改成cnpm。使用本地安装的好处是,当升级项目时会比较方便。如果正确执行了上面的两个命令,会在本地目录多出一个node_modules目录和一个package.json文件。打开package.json文件,内容如下:package.json{ “devDependencies”: { “webpack”: “^4.27.1”, “webpack-cli”: “^3.1.2” }}这就是我们刚才安装依赖。此时的package.json只有3行的代码,如果是第一次接触webpack并且是第一次接触node的朋友,会被手册中接下来的代码产生疑问。因为接下来手册添加了一段scripts代码。我们在项目中,添加这段代码之前,先来执行下面这个命令:npm init这个命令会初始化一个新的package.json,因为我们在安装webpack时生成了一个package.json,所以接下来的内容会直接追加到文件中。执行npm init这个命令之后,会向你提问一系列问题,如果觉得默认信息没有问题的话,直接回车就行。新生成的package.json类似如下:package.json{ “devDependencies”: { “webpack”: “^4.27.1”, “webpack-cli”: “^3.1.2” }, “name”: “webpackstudy”, “description”: “webpack入门学习手记”, “version”: “1.0.0”, “main”: “index.js”, “dependencies”: { “ajv-errors”: “^1.0.1”, … }, “scripts”: { “test”: “echo "Error: no test specified" && exit 1” }, “repository”: { “type”: “git”, “url”: “git@git.dev.tencent.com:siberiawolf0307/webpackStudy.git” }, “author”: “siberiawolf”, “license”: “MIT”}因为生成的dependencies内容过多,我直接省略掉了。此时就能找到指南手册中的scripts了。我们在默认的test后面添加上官网中的代码即可。如下:package.json “scripts”: { “test”: “echo "Error: no test specified" && exit 1”, “start”: “webpack –config webpack.config.js” }补充:在使用默认package.json是,会让用户填写license。因为对开源协议比较陌生,我特意去查了一下使用何种开源协议。经过对比,我发现使用 MIT 协议是比较适合我的。MIT 协议是比较宽松的协议。此协议允许别人以任何方式使用你的代码同时署名原作者,但原作者不承担代码使用后的风险,当然也没有技术支持的义务。jQuery和Rails就是 MIT 协议。上面的添加scripts.start,其实是运行webpack,并且运行的配置文件是webpack.config.js。因为手册进行到这里,并没有添加任何的 js 文件,所以先不要执行这个命令~ 我们后面再来学习。说明:我将本小节代码托管到了腾讯云开发者平台,如果需要查看这节内容,请查找Installation目录即可。开始如果你真的动手操作了一遍安装步骤,并且是初学者,那么请先删除掉你文件夹下的package.json文件。因为接下来按照手册给出的示例,我们是在一个全新的目录下进行操作的,请注意这一点。因为我们在执行安装小节是,肯定已经在一个工作目录下,例如我的工作目录是webpackStudy,所以文档中创建新目录mkdir webpack-demo && cd webpack-demo省略掉。下面的代码示例都是在这个工作目录下。因为已经删除掉了package.json文件,接下来我们新生成一个:npm init -ynpm install webpack webpack-cli –save-devnpm init -y,多了一个参数-y,就是说不用再询问我们填写参数了,直接使用了默认值。另外因为在本工作目录下已经安装过webpack了,此时执行的速度会非常快。接下来,我们创建相应的html、js文件。project webpackStudy |- package.json+ |- index.html+ |- /src+ |- index.jssrc/index.jsfunction component() { let element = document.createElement(‘div’); // Lodash, currently included via a script, is required for this line to work // 这段英文注释的意思是说,在index.html文件中已经引入了Lodash这个script标签了,所以能正常使用 element.innerHTML = _.join([‘Hello’, ‘webpack’], ’ ‘); return element;}document.body.appendChild(component());index.html<!doctype html><html> <head> <title>Getting Started</title> <script src=“https://unpkg.com/lodash@4.16.6"></script> </head> <body> <script src=”./src/index.js"></script> </body></html>在工作目录下创建index.html文件,并创建src目录,然后创建index.js文件。文件的组织形式如上。然后复制粘贴html和js代码。接下来我们修改一下package.json文件,删掉main入口,并设置private,防止意外发布代码。package.json{ “description”: “”,+ “private”: true,- “main”: “index.js”}需要注意的是,在html页面中,我们引入了Lodash这个js文件。Lodash是一个JS实用工具库,非常适合于遍历数组、字符串和对象等。在index.js文件中,并没有显示的声明需要引入Lodash。这样就会造成以下几个问题:没有显示声明,index.js中的代码依赖于外部的扩展库。如果依赖不存在,或者引入错误,应用程序无法正常执行。例如没有引用Lodash。如果依赖文件被引入了,但是没有使用,浏览器就会下载无用代码。如果我们使用webpack来管理JS呢?看看情况如何创建一个打包文件首先调整一下我们的工作目录。创建一个dist目录,用来存放压缩和优化之后的代码。而我们之前创建的src目录,用来存放原始代码。将之前创建的index.html文件移动到dist目录下。最终文件结构如下:projectwebpackStudy |- package.json+ |- /dist+ |- index.html- |- index.html |- /src |- index.js因为我们要在index.js中显示声明Lodash,所以需要先在项目目录下安装好,执行命令:npm install –save-dev lodash注意这里,我使用的是–save-dev参数。这样会将Lodash添加到package.json的devDependencies属性下。接下来,通过import命令,显示引用Lodash。src/index.js+ import _ from ’lodash’;+ function component() { let element = document.createElement(‘div’);- // Lodash, currently included via a script, is required for this line to work element.innerHTML = _.join([‘Hello’, ‘webpack’], ’ ‘); return element; } document.body.appendChild(component())然后需要更新下index.html文件,因为我们修改了依赖js的方式。dist/index.html<!doctype html> <html> <head> <title>Getting Started</title>- <script src=“https://unpkg.com/lodash@4.16.6"></script> </head> <body>- <script src=”./src/index.js"></script>+ <script src=“main.js”></script> </body> </html>最后,我们执行下面的命令:npx webpack然后打开index.html文件,就会看到 Hello webpack。 因为我们在执行npx webpack时,没有使用执行的配置文件,所以使用默认值,也就是会在dist目录下生成main.js。而这个打包之后的文件,就在index.html引入。如果打开main.js,会发现Lodash已经在这个文件中了。webpack已经帮我们添加好了。指定配置文件在webpack4 中,不用指定配置文件。但是这样可扩展性就差了。所以我们创建一个webpack.config.js文件。在工作目录下创建webpack.config.js文件。内容如下:webpack.config.jsconst path = require(‘path’);module.exports = { entry: ‘./src/index.js’, output: { filename: ‘main.js’, path: path.resolve(__dirname, ‘dist’) }};然后执行如下命令:npx webpack –config webpack.config.js执行完之后结果跟之前一样其实也可以不指定–config webpack.config.js这个参数。如果添加了webpack.config.js文件,webpack会自动使用这个配置文件。但是假如文件的名字不是webpack.config.js时,就需要用到–config这个参数了。尤其是当配置文件被拆分成多个文件时,会非常有用。之前我们直接在命令行中运行的方式叫做CLI,现在通过配置文件,具备更高的灵活性。可以指定loader、plugins 等。NPM 脚本还记得在开始小节中,我们添加的scripts.start吗?现在我们同样的在package.json中添加一段脚本,这样我们每次运行程序是,只需要简单调用脚本即可。package.json"scripts": { “test”: “echo "Error: no test specified" && exit 1”, “build”: “webpack –config webpack.config.js” }然后在命令行执行如下命令:npm run buildnpm run build这段命令,就是制定package.json中的scripts脚本,其中build就是我们刚才定义的内容。说明:我将本小节代码托管到了腾讯云开发者平台,如果需要查看这节内容,请查找Getting Started目录即可。以上就是指南手册中的Getting Started部分。总结一下主要内容:安装webpack和依赖的js工具库Lodash通过默认配置和指定配置文件,分别打包文件使用NPM 脚本运行webpack下一篇笔记整理webpack官方文档的指南手册剩余部分,敬请关注。(待续) ...

December 15, 2018 · 2 min · jiezi