webpack打包

webpack打包指令npm run build使用webpack打包vue项目,需要更改配置1、webpack打包后生成dist文件,将dist文件更改成project,放在服务器中,会报错js的引用路径不对,页面空白。解决办法:找到 config > index.jsbuild: { index: path.resolve(__dirname, ‘../dist/index.html’), assetsRoot: path.resolve(__dirname, ‘../dist’), assetsSubDirectory: ‘static’, // assetsPublicPath: ‘/’, // 在/后边+文件名 assetsPublicPath: ‘/project’,2、图片路径不对举例:在src > assets > img > index.jpg 放置图片在src > components > home > Home.vue 引用.home-bg background-image: url("../../assets/img/index.jpg")打包生成文件后,图片加载路径会出问题。解决办法:找到 build > utils.jsif (options.extract) { return ExtractTextPlugin.extract({ use: loaders, fallback: ‘vue-style-loader’, // 添加publicPath: ‘../../’ publicPath: ‘../../’ }) } else { return [‘vue-style-loader’].concat(loaders) }再次打包就可以了

December 13, 2018 · 1 min · jiezi

webpack入门学习手记(一)

本人公众号:前端修炼之路,欢迎关注。之前用过gulp、grunt,但是一直没有学习过webpack。这两天刚好有时间,学习了下webpack。webpack要想深入研究,配置的东西比较多,网上的资源也有很多。我这里学习的主要途径是webpack官方给出的指南,和webpack中文网的翻译版本。因为我觉得第一手资料肯定是官网给出的更权威一些。我学习的过程是,先看一遍中文网的文章,对每一节的内容有个大致印象和理解;然后再看一遍英文的官方文档,按照官方文档给出的示例配置文件照着做一遍。如果哪里英文理解有问题,再照着中文的文档反复思考一下。所以我的这篇文章,也有类似翻译英文官方文档。因为我就说照着文档操作,然后再把这个过程按照自己的理解重新整理成文章。之所以这么做的原因是,一方面要提高自己的英文文档阅读和理解能力,另一方面是中文的文档一般都会更新得比较滞后和有不少错误,不能光按照中文手册去做。最后就是肯定要动手自己操作一遍的,理解起来是一回事儿,操作起来就是另外一回事儿了。在这个过程中,主要有一下几点心得:后悔没有早点学webpack,功能太强大了。webpack功能和概念真多,感觉一下子学不完,只能用啥学啥。先整理出主要内容,细节一点点学习、补充。通过一段时间的锻炼,阅读英文文档能力有所提高,需要继续努力。争取早日能完全抛弃掉中文文档,最终可以翻译英文文档,输出英文文档。以下是正文~概念在开始之前,必须要知道webpack涉及的概念。目前我学习webpack是最新的版本是v4.27.1,另外官网明确指出,从webpack 4 以上开始,就不在需要必须制定配置文件,但是仍然具备可扩展性。为了学习webpack,需要理解的核心概念:Entry:入口Output:输出Loaders:loaderPlugins:插件Mode:模式Browser Compatibility:浏览器兼容Entryentry说简单点,就是没有打包之前的原文件。可以指定一个文件、可以指定多个文件或者不同目录下的文件。入股不指定,默认值为:./src/index.js。在配置文件中指定其他文件时,例如:module.exports = { entry: ‘./path/to/my/entry/file.js’};Outputoutput属性告诉webpack在哪里输出打包好的文件,以及如何命名这个文件。默认情况下是./dist/main.js,作为主要的输出文件,./dist目录就是输出的目录。可以在配置文件中修改output属性来修改输出文件和目录,例如:webpack.config.jsconst path = require(‘path’);module.exports = { entry: ‘./path/to/my/entry/file.js’, output: { path: path.resolve(__dirname, ‘dist’), filename: ‘my-first-webpack.bundle.js’ }};上面例子中,使用output.filename和output.path属性,告诉webpack打包文件的名字和打包文件的目录。其中的path模块是,Node.js模块。Loaderswebpack只能识别JavaScript和JSON文件,Loaders允许webpack处理其他类型的文件。在webpack配置文件中,需要指定一下两个属性test:test属性告诉webpack哪些文件需要被转换。use:use属性告诉webpack相应的文件使用哪个loader进行转换。例如:webpack.config.jsconst path = require(‘path’);module.exports = { output: { filename: ‘my-first-webpack.bundle.js’ }, module: { rules: [ { test: /.txt$/, use: ‘raw-loader’ } ] }};上面的配置中定义了一个module.rules属性,这个属性又有两个属性:test和use。这就好像告诉webpack编译器说:”Hi,webpack编译器,当你发现任何后缀为.txt的文件时,请使用raw-loader先转换一下,然后再把转换后的内容添加到打包文件中。“PluginsLoaders是用来转换某些类型的模块,而插件可以做更广泛的工作,例如压缩、优化程序,甚至改变环境变量。想使用一个插件,只需要通过require()这个插件,然后在plugins数组中添加这个插件。大多数的插件,都是支持修改配置的。例如:webpack.config.jsconst HtmlWebpackPlugin = require(‘html-webpack-plugin’); //installed via npmconst webpack = require(‘webpack’); //to access built-in pluginsmodule.exports = { module: { rules: [ { test: /.txt$/, use: ‘raw-loader’ } ] }, plugins: [ new HtmlWebpackPlugin({template: ‘./src/index.html’}) ]};在上面的例子中,使用html-webpack-plugin生成一个HTML文件,这个文件就是你的应用程序。在其中已经自动引用好了打包的文件。Mode通过设置mode属性,可以启动webpack内置的优化。你可以指定development、production、none,分别对应着不同的环境。默认的是production。例如:webpack.config.jsmodule.exports = { mode: ‘production’};Browser Compatibilitywebpack 支持所有基于ES5的浏览器,但是IE8及以下是不支持的。因为webpack需要import()和require()。如果需要支持老版本浏览器,可以使用loader解决。以上就是webpack的核心概念。下一篇笔记整理webpack官方文档的指南手册,敬请关注。(未完) ...

December 12, 2018 · 1 min · jiezi

加快Vue项目的开发速度

现如今的开发,比如是内部使用的管理平台这种项目大都时间比较仓仓促。实际上来说在使用了webpack + vue 这一套来开发的话已经大大了提高了效率。但是对于我们的开发层面。还是有很多地方可以再次提高我们的项目开发效率,让我们更加专注于业务,毕竟时间就是生命。下面我们挨个来探讨。巧用WebpackWebpack是实现我们前端项目工程化的基础,但其实她的用处远不仅仅如此,我们可以通过Webpack来帮我们做一些自动化的事情。首先我们要了解require.context()这个APIrequire.context()您可以使用require.context()函数创建自己的上下文。 它允许您传入一个目录进行搜索,一个标志指示是否应该搜索子目录,还有一个正则表达式来匹配文件。其实是Webpack通过解析 require() 的调用,提取出来如下这些信息:Directory: ./templateRegular expression: /^..ejs$/然后来创建我们自己的上下文,什么意思呢,就是我们可以通过这个方法筛选出来我们需要的文件并且读取下面我们来简单看一看使用:/** @param directory 要搜索的文件夹目录不能是变量,否则在编译阶段无法定位目录* @param useSubdirectories 是否搜索子目录* @param regExp 匹配文件的正则表达式* @return function 返回一个具有 resolve, keys, id 三个属性的方法 resolve() 它返回请求被解析后得到的模块 id keys() 它返回一个数组,由所有符合上下文模块处理的请求组成。 id 是上下文模块里面所包含的模块 id. 它可能在你使用 module.hot.accept 的时候被用到*/require.context(‘demo’, useSubdirectories = false, regExp = /.js$/)// (创建了)一个包含了 demo 文件夹(不包含子目录)下面的、所有文件名以 js 结尾的、能被 require 请求到的文件的上下文。不要困惑,接下来我们来探讨在项目中怎么用。组织路由对于Vue中的路由,大家都很熟悉,类似于声明式的配置文件,其实已经很简洁了。现在我们来让他更简洁分割路由首先为了方便我们管理,我们把router目录下的文件分割为以下结构router // 路由文件夹 |__index.js // 路由组织器:用来初始化路由等等 |__common.js // 通用路由:声明通用路由 |__modules // 业务逻辑模块:所以的业务逻辑模块 |__index.js // 自动化处理文件:自动引入路由的核心文件 |__home.js // 业务模块home:业务模块 |__a.js // 业务模块a modules文件夹中处理业务模块modules文件夹中存放着我们所有的业务逻辑模块,至于业务逻辑模块怎么分,我相信大家自然有自己的一套标准。我们通过上面提到的require.context()接下来编写自动化的核心部分index.js。const files = require.context(’.’, true, /.js$/)console.log(files.keys()) // ["./home.js"] 返回一个数组let configRouters = []// files.keys().forEach(key => {if (key === ‘./index.js’) returnconfigRouters = configRouters.concat(files(key).default) // 读取出文件中的default模块}) export default configRouters // 抛出一个Vue-router期待的结构的数组自动化部分写完了,那业务组件部分怎么写? 这就更简单了import Frame from ‘@/views/frame/Frame’ import Home from ‘@/views/index/index’ export default [ // 首页 { path: ‘/index’, name: ‘首页’, redirect: ‘/index’, component: Frame, children: [ // 嵌套路由 { path: ‘’, component: Home } ] }]3. common路由处理 我们的项目中有一大堆的公共路由需要处理比如404阿,503阿等等路由我们都在common.js中进行处理。export default [// 默认页面{ path: ‘/’, redirect: ‘/index’, hidden:true},// 无权限页面{ path: ‘/nopermission’, name: ’nopermission’, component: () => import(’@/views/NoPermission’)},// 404{ path: ‘’, name: ’lost’, component: () => import(’@/views/404’)}]4. 路由初始化这是我们的最后一步了,用来初始化我们的项目路由import Vue from ‘vue’ import VueRouter from ‘vue-router’ import RouterConfig from ‘./modules’ // 引入业务逻辑模块 import CommonRouters from ‘./common’ // 引入通用模块 Vue.use(VueRouter) export default new VueRouter({mode: ‘history’,// 需要服务端支持scrollBehavior: () => ({ y: 0 }),routes: RouterConfig.concat(CommonRouters)})估计有些朋友代码写到这还不知道到底这样做好处在哪里。我们来描述一个场景,比如按照这种结构来划分模块。正常的情况是我们创建完home.js要手动的把这个模块import到路由文件声明的地方去使用。但是有了上面的index.js,在使用的时候你只需要去创建一个home.js并抛出一个符合VueRouter规范的数组,剩下的就不用管了。import RouterConfig from './modules' // 引入业务逻辑模块 已经帮你处理完了。另外扩展的话你还可以把hooks拿出来作为一个单独文件。### 全局组件统一声明同样的道理,有了上面的经验,我们照葫芦画瓢来处理一下我们的全局组件。这就没什么可说的了,直接上核心代码1. 组织结构components // 组件文件夹|__xxx.vue // 其他组件|__global // 全局组件文件夹 |__index.js // 自动化处理文件 |__demo.vue // 全局demo组件2. global处理import Vue from ‘vue’ let contexts = require.context(’.’, false, /.vue$/) contexts.keys().forEach(component => {let componentEntity = contexts(component).default// 使用内置的组件名称 进行全局组件注册Vue.component(componentEntity.name, componentEntity)})3. 使用和说明这个使用起来就更简单了,直接在app.js引用这个文件就行。注意:我之前看到有些人做法是使用组件名去区分全局组件和普通组件,然后通过正则去判断需不需要全局注册。我是直接把全局的组件放到global文件夹下,然后组件的注册名称直接使用component.name。至于使用哪种方式就比较看个人了。## 充分利用NodeJS放着node这么好得东西不用真是有点浪费,那么我们来看看node能为我们增加效率做出什么贡献。有这么一个场景,我们每次创建模块的时候都要新建一个vue文件和对应的router配置,而且新页面的大部分东西都还差不多,还得去复制粘贴别得页面。这想想就有点low。那既然有了node我们可不可以通过node来做这写乱七八糟得事情? 下面来把我们的想法付诸于显示。我们实现这个功能主要要借助Node的fs和process, 感兴趣的话可以深入研究一下。首先我们要编写我们的node脚本,这里是一个比较简单的版本。什么验证文件夹或者文件的都没有,只是来实现我们这个想法:/fast add new module script/const path = require(‘path’)const fs = require(‘fs’)const chalk = require(‘chalk’)const reslove = file => path.resolve(__dirname, ‘../src’, file)// symbol constconst RouterSymbol = Symbol(‘router’), ViewsSymbol = Symbol(‘views’)// root pathconst rootPath = {}//loggsconst errorLog = error => console.log(chalk.red(${error}))const defaultLog = log => console.log(chalk.green(${log}))// module namelet moduleName = new String()let fileType = new String()//const stringconst vueFile = module => (&lt;template&gt;&lt;/template&gt;&lt;script&gt;export default { name: '${module}', data () {return {}}, methods: {}, created() {}}&lt;/script&gt;&lt;style lang="less"&gt;&lt;/style&gt;)// route fileconst routerFile = module => (// write your comment here...export default [ {path: '/${module}',name: '',redirect: '/${module}',component: () =&gt; import('@/views/frame/Frame'),children: [ { path: '', fullPath: '', name: '', component: () =&gt; import('@/views/${module}/index') }]}])/**generate file@param {} filePath@param {} content@param {} dirPath/const generateFile = async (filePath, content, dirPath = ‘’) =>{ try {// create file if file not exitif (dirPath !== ’’ && ! await fs.existsSync(dirPath)) { await fs.mkdirSync(dirPath) defaultLog(created ${dirPath})}if (! await fs.existsSync(filePath)) { // create file await fs.openSync(filePath, ‘w’) defaultLog(created ${filePath})}await fs.writeFileSync(filePath, content, ‘utf8’)} catch (error) {errorLog(error)}}// module-method mapconst generates = new Map([ [‘view’, async (module) => {// module fileconst filePath = path.join(rootPath[ViewsSymbol], module)const vuePath = path.join(filePath, ‘/index.vue’)await generateFile(vuePath, vueFile(module), filePath)}], // router is not need new folder [‘router’,async (module) => {const routerPath = path.join(rootPath[RouterSymbol], /${module}.js)await generateFile(routerPath, routerFile(module))}]])defaultLog(请输入模块名称(英文):)// filesconst files = [‘view’, ‘router’]// 和命令行进行交互 获取的创建的模块名称process.stdin.on(‘data’, (chunk) => { try {if (!moduleName) { moduleName = chunk} else { chunk = chunk.slice(0,-2) // delete /n defaultLog(new module name is ${chunk}) files.forEach(async (el, index) => { // 执行创建语句 await generates.get(${el}).call(null, chunk.toString()) if (index === files.length-1) { process.stdin.emit(’end’) } })}} catch (error) {errorLog(error)}})process.stdin.on(’end’, () => { defaultLog(‘create module success’)})下面我们看使用的流程这样我们就分别创建了vue和router的文件,而且已经注入了内容。按照我们提前声明的组件注意:这只是一个简单的思路,通过Node强大的文件处理能力,我们能做的事情远不止这些。## 发挥Mixins的威力Vue中的混入mixins是一种提供分发 Vue 组件中可复用功能的非常灵活的方式。听说在3.0版本中可能会用Hooks的形式实现,但这并不妨碍它的强大。基础部分的可以看这里。这里主要来讨论mixins能在什么情景下帮助我们。比如我们的大量的表格页面,仔细一扒拉你发现非常多的东西都是可以复用的例如分页,表格高度,加载方法, laoding声明等一大堆的东西。下面我们来整理出来一个简单的list.vueconst list = { data () {return { // 这些东西我们在list中处理,就不需要在每个页面再去手动的做这个了。 loading: false, // 伴随loading状态 pageNo: 1, // 页码 pageSize: 15, // 页长 totalCount: 0, // 总个数 pageSizes: [15, 20, 25, 30], //页长数 pageLayout: ’total, sizes, prev, pager, next, jumper’, // 分页布局 list: []}}, methods: {// 分页回掉事件handleSizeChange(val) { this.pageSize = val // todo},handleCurrentChange (val) { this.pageNo = val // todo},/** * 表格数据请求成功的回调 处理完公共的部分(分页,loading取消)之后把控制权交给页面 * @param {} apiResult * @returns {} promise /listSuccessCb (apiResult = {}) { return new Promise((reslove, reject) => { let tempList = [] // 临时list try { this.loading = false // todo // 直接抛出 reslove(tempList) } catch (error) { reject(error) } })},/* * 处理异常情况 * ==> 简单处理 仅仅是对表格处理为空以及取消loading */listExceptionCb (error) { this.loading = false console.error(error)}}, created() {// 这个生命周期是在使用组件的生命周期之前this.$nextTick().then(() => { // todo})}}export default list下面我们直接在组件中使用这个mixinsimport mixin from ‘@/mixins/list’ // 引入import {getList} from ‘@/api/demo’export default { name: ‘mixins-demo’, mixins: [mixin], // 使用mixins data () {return {}}, methods: {// 加载列表load () { const para = { } this.loading = true getList(para).then((result) => { this.listSuccessCb(result).then((list) => { this.list = list }).catch((err) => { console.log(err) }) }).catch((err) => { this.listExceptionCb(err) })}}, created() {this.load()}}</script>使用了mixins之后一个简单的有loadoing, 分页,数据的表格大概就只需要上面这些代码。注意: <font color=“red”>mixins它固然是简单的,但是注释和引用一定要做好,不然的话新成员进入团队大概是一脸的懵逼,而且也不利于后期的维护。也是一把双刃剑。另外:全局mixins一定要慎用,如果不是必须要用的话我还是不建议使用。</font>## 进一步对组件进行封装大家都知道组件化的最大的好处就是高度的可复用性和灵活性。但是组件怎么封装好,封装到什么程度让我们更方便。这是没有标准的答案的。我们只有根据高内聚,低耦合的这个指导思想来对我们的业务通用组件来进行封装,让我们的业务页面结构更加的简洁,加快我们的开发效率。封装多一点的话页面可能会变成这样:<template> <box-content><!– 头部标题部分 –><page-title> <bread slot=“title” :crumbs="[{name: ‘xx管理’, path: ‘’, active: true, icon: ‘’}, {name: ‘xxxx’, path: ‘’, active: true, icon: ‘’}]"></bread></page-title><!– 表格部分 –><div> <base-table v-loading=“loading” :columns=“headers” :list=“list” :page-no =“pageNo” :page-size=“pageSize” :total-count=“totalCount” @delete=“deleteItm” @change-size=“handleSizeChange” @change-page=“handleCurrentChange”> </base-table></div></box-content></template>有什么东西一目了然。### 无状态组件最容易勾起我们封装欲望的就是无状态HTML组件,例如我们除去header, menu之后的content部分。没有什么需要复杂的交互,但是我们每个页面又都得写。你说不拿它开刀拿谁开????<template> <div class=“container-fluid” :class="[contentClass]"> <el-row> <el-col :span=“24”> <!– box with #fff bg –> <div class=“box”> <div class=“box-body”> <slot></slot> </div> </div> </el-col> </el-row></div></template>上面这个处理非常的简单,但是你在项目中会非常频繁的使用过到,那么这个封装就很有必要了。### ElementUI table组件封装ElementUI中得组件其实已经封装得很优秀了,但是表格使用得时候还是有一堆得代码在我看来是不需要在业务中重复写得。封装到靠配置来进行表格得书写得一步我觉得就差不多了,下面是一个小demo<template> <el-row><el-col :span=“24”> <el-table :data=“list” border size=“mini” @selection-change=“handleSelectionChange” :max-height=“tableHeight” v-bind="$attrs"> <!– –> <template v-for="(column, index) in columns"> <slot name=“front-slot”> </slot> <!– 序号 –> <el-table-column :key=“index” v-if=“column.type === ‘selection’” type=“selection” width=“55”> </el-table-column> <!– 复选框 –> <el-table-column :key=“index” v-else-if=“column.type === ‘index’” type=“index” width=“50” label=“序号”> </el-table-column> <!– 具体内容 –> <el-table-column :key=“index” v-else align=“left” :label=“column.title” :width=“column.width”> <template slot-scope=“scope”> <!– 仅仅显示文字 –> <label v-if="!column.hidden"> <!– 如果hidden为true的时候 那么当前格可以不显示,可以选择显示自定义的slot–> <!– 操作按钮 –> <label v-if=“column.type === ‘operate’"> <a href=“javascript:void(0)” class=“operate-button” v-for="(operate, index) in column.operates” :key=“index” @click=“handleClick(operate, scope.row)"> {{operate.name}} &nbsp;&nbsp; </a> </label> <span v-else> {{scope.row[column.key]}} </span> </label> <!– 使用slot的情况下 –> <label v-if=“column.slot”> <!– 具名slot –> <slot v-if=“column.slot” :name=“column.slot” :scope=“scope”></slot> </label> </template> </el-table-column> </template> <!–默认的slot –> <slot/> </el-table></el-col></el-row></template>export default { name: ‘base-table’, props: {// 核心数据list: { type: Array, default: () => []},// columnscolumns: { type: Array, required: true, default: () => []}}, data () {return { tableHeight: xxx}}, methods: {// 处理点击事件handleClick(action, data) { // emit事件 this.$emit(${action.emitKey}, data)}}}使用:<base-table v-loading=“loading” :columns=“headers” :list=“list” @view=“viewCb”> <!– 自定义的slot –> <template slot=“demoslot” slot-scope="{scope}"><span> {{scope.row}}</span></template> <!– 默认的slot 如果交互很复杂 我们还可以直接使用表格内部的组件 –> <el-table-columnlabel=“操作"width=“200”<template slot-scope=“scope”> <a href=“javascript:void(0)” @click=“defaultSlot(scope.row)">xxx</a></template></el-table-column></base-table>export default { name: ’table-demo’, data () {return { // 表格头部配置 headers: [ { key: ‘xxx’, title: ‘测试’ }, { title: ‘xxx’, hidden: true, slot: ‘demoslot’}, { title: ‘操作’, type: ‘operate’, operates: [ {name: ‘详情’,emitKey: ‘view’} ] } ]}}, methods: {viewCb(){ // todo},defaultSlot(){ // todo}}}这样封装过的表格,应付基本的一些需求问题应该不大。至于特殊的要求可以一步一步的进行完善。# 总结这些东西并不是什么语法糖,是真正可以在项目中加快我们的效率。让我们的自己乃至整个团队从繁杂的重复复制粘贴中解脱一点。至于速度和质量的问题。我是觉得使用公共组件质量可控性会更高一些。我建议公共得东西注释一定要写得全面和详细,这样可以极大的降低我们的交流成本。至于组件的封装还是要看你的业务。以上观点纯属个人意见,如有错误,多谢指正。示例代码还在整理中。。。 ...

December 12, 2018 · 5 min · jiezi

React16.7 hooks初试之setTimeout引发的bug

前言 周末尝试了一下React新的hooks功能,来封装一个组件,遇到一个bug,所以记录一下过程!报错如下:Warning: Can’t perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.in Notification大概意思是组件已经卸载了,但在卸载之后还执行了一个对组件更新的操作,这是一个无效的操作,但它表示应用程序中存在内存泄漏。要修复,请取消useEffect cleanup function.in Notification 中的所有订阅和异步任务组件核心代码如下:function Notification(props){ var timer = null; const [visible, setVisible] = useState(false); let {title,description,duration,theme,onClose,}= props; let leave = (source=’’) => { clearTimeout(timer); setVisible(false); console.log(“注意这里是 leave方法里,timer的id:"+timer,“事件的来源:",source); console.log(“leave result:",timer); onClose&&onClose(); } let enter = () => { setVisible(true); if( duration > 0 ){ let timer = setTimeout(() => { console.log(auto carried out,timer) //timer Number Id leave(Time to); }, duration1000); console.log(enter方法里,timer的id:,timer) //timer Number Id } } useEffect(()=>{ enter(); },[]) return ( <div className={${prefixCls}-notice} style={{display:${visible?'':'none'}}}> {!!theme&&<p className={${prefixCls}-notice-icon}><Svg iconId={svg-${theme}} /></p>} <div className={${prefixCls}-notice-content}> ……//首席填坑官∙苏南的专栏 交流:912594095、公众号:honeyBadger8 </div> <p className={${prefixCls}-notice-colse} title=“关闭” onClick={()=>leave(“手动点击的关闭”)}><Svg/></p> </div> );};简单分析:首先useEffect方法,是react新增的,它是componentDidMount,componentDidUpdate、componentWillUnmount三个生命周期的合集,也就是之前的写法,上面三生命周期里会执行到的操作,useEffect都会去做;enter、leave方法很好理解,进场、出场两函数,进场:加了个定时器,在N秒后执行出场即leave方法,这个逻辑是正常的,问题就出在手动执行leave,也就是onclick事件上,问题原因:其实就是在点击事件的时候,没有获取到 timer的id,导致了定时器没有清除掉;!!看图说话:解决思路:当然是看官方文档,hooks对我来说也是个新玩意,不会~1、useEffect方法里return 一个方法,它是可以在组件卸载时执行的,2、清除定时器它有自己的方式,const intervalRef = useRef();指定赋值后能同步更新,之前的timer手动执行没有拿到timer所以没有清除掉;参考链接:中文,英文的没有找到文档英文的也补一下吧react github也有人提到这个问题,学习了完美解决:function Notification(props){ var timer = null; const [visible, setVisible] = useState(false); let {title,description,duration,theme,onClose,}= props; const intervalRef = useRef(null); let leave = (source=’’) => { clearTimeout(intervalRef.current); setVisible(false); console.log(“leave result:",source,intervalRef); onClose&&onClose(); } let enter = () => { setVisible(true); if( duration > 0 ){ let id = setTimeout(() => { console.log(auto carried out,intervalRef) //timer Number Id leave(Time to); }, duration1000);//首席填坑官∙苏南的专栏 交流:912594095、公众号:honeyBadger8 intervalRef.current = id; } } useEffect(()=>{ enter(); return ()=>clearTimeout(intervalRef.current); },[]) return ( <div className={${prefixCls}-notice} style={{display:${visible?'':'none'}}}> {!!theme&&<p className={${prefixCls}-notice-icon}><Svg iconId={svg-${theme}} /></p>} <div className={${prefixCls}-notice-content}> ……//首席填坑官∙苏南的专栏 交流:912594095、公众号:honeyBadger8 </div> <p className={${prefixCls}-notice-colse} title=“关闭” onClick={()=>leave(“手动点击的关闭”)}><Svg/></p> </div> );};热门推荐资源共享,一起学习团队解散,我们该何去何从?如何给localStorage设置一个有效期?作者:苏南 - 首席填坑官链接:https://blog.csdn.net/weixin_…交流:912594095、公众号:honeyBadger8本文原创,著作权归作者所有。商业转载请联系@IT·平头哥联盟获得授权,非商业转载请注明原链接及出处。 ...

December 6, 2018 · 2 min · jiezi

通用、封装、简化 webpack 配置

通用、封装、简化 webpack 配置现在,基本上前端的项目打包都会用上 webpack,因为 webpack 提供了无与伦比强大的功能和生态。但在创建一个项目的时候,总是免不了要配置 webpack,很是麻烦。简化 webpack 配置的一种方式是使用社区封装好的库,比如 roadhog。roadhog 封装了 webpack 的一些基础配置,然后暴露一些额外配置的接口,并附加本地数据模拟功能(mock),详情可以参考 roadhog 主页。另一种方式是自己封装 webpack,这样做自己能够更好的掌控项目。1. 要封装哪些功能一般搭建一个项目至少需要两种功能:本地开发调试、构建产品代码。其他的诸如测试、部署到服务器、代码检查、格式优化等功能则不在这篇文章讲解范围,如果有意了解,可以查看我的其他文章。2. 基础配置2.1 目录结构(示例,配合后面的代码讲解)package.jsondev.js # 本地开发脚本build.js # 产品构建脚本analyze.js # 模块大小分析(可选)# 单页面结构src/ # 源代码目录 - index.js # js 入口文件 - index.html # html 入口文件 - … # 其他文件# 多页面结构src/ # 源代码目录 - home/ # home 页面工作空间 - index.js # home 页面 js 入口文件 - index.html # home 页面 html 入口文件 - … # home 页面其他文件 - explore/ # explore 页面工作空间 - index.js # explore 页面 js 入口文件 - index.html # explore 页面 html 入口文件 - … # explore 页面其他文件 - about/ # about 目录 - company # about/company 页面工作空间 - index.js # about/company 页面 js 入口文件 - index.html # about/company 页面 html 入口文件 - … # about/company 页面其他文件 - platform # about/platform 页面工作空间 - index.js # about/platform 页面 js 入口文件 - index.html # about/platform 页面 html 入口文件 - … # about/platform 页面其他文件 - … # 更多页面 2.2 基础 npm 包# package.json"devDependencies": { “@babel/core”: “^7.1.2”, # babel core “@babel/plugin-syntax-dynamic-import”: “^7.0.0”, # import() 函数支持 “@babel/plugin-transform-react-jsx”: “^7.0.0”, # react jsx 支持 “@babel/preset-env”: “^7.1.0”, # es6+ 转 es5 “@babel/preset-flow”: “^7.0.0”, # flow 支持 “@babel/preset-react”: “^7.0.0”, # react 支持 “autoprefixer”: “^9.1.5”, # css 自动添加厂家前缀 -webkit-, -moz- “babel-loader”: “^8.0.4”, # webpack 加载 js 的 loader “babel-plugin-component”: “^1.1.1”, # 如果使用 element ui,需要用到这个 “babel-plugin-flow-runtime”: “^0.17.0”, # flow-runtime 支持 “babel-plugin-import”: “^1.9.1”, # 如果使用 ant-design,需要用到这个 “browser-sync”: “^2.24.7”, # 浏览器实例组件,用于本地开发调试 “css-loader”: “^1.0.0”, # webpack 加载 css 的 loader “chalk”: “^2.4.1”, # 让命令行的信息有颜色 “file-loader”: “^2.0.0”, # webpack 加载静态文件的 loader “flow-runtime”: “^0.17.0”, # flow-runtime 包 “html-loader”: “^0.5.5”, # webpack 加载 html 的 loader “html-webpack-include-assets-plugin”: “^1.0.5”, # 给 html 文件添加额外静态文件链接的插件 “html-webpack-plugin”: “^3.2.0”, # 更方便操作 html 文件的插件 “less”: “^3.8.1”, # less 转 css “less-loader”: “^4.1.0”, # webpack 加载 less 的 loader “mini-css-extract-plugin”: “^0.4.3”, # 提取 css 单独打包 “minimist”: “^1.2.0”, # process.argv 更便捷处理 “node-sass”: “^4.9.3”, # scss 转 css “optimize-css-assets-webpack-plugin”: “^5.0.1”, # 优化 css 打包,包括压缩 “postcss-loader”: “^3.0.0”, # 对 css 进行更多操作,比如添加厂家前缀 “sass-loader”: “^7.1.0”, # webpack 加载 scss 的 loader “style-loader”: “^0.23.0”, # webpack 加载 style 的 loader “uglifyjs-webpack-plugin”: “^2.0.1”, # 压缩 js 的插件 “url-loader”: “^1.1.1”, # file-loader 的升级版 “vue-loader”: “^15.4.2”, # webpack 加载 vue 的 loader “vue-template-compiler”: “^2.5.17”, # 配合 vue-loader 使用的 “webpack”: “^4.20.2”, # webpack 模块 “webpack-bundle-analyzer”: “^3.0.2”, # 分析当前打包各个模块的大小,决定哪些需要单独打包 “webpack-dev-middleware”: “^3.4.0”, # webpack-dev-server 中间件 “webpack-hot-middleware”: “^2.24.2” # 热更新中间件}2.3 基本命令# package.json"scripts": { “dev”: “node dev.js”, “build”: “node build.js”, “analyze”: “node analyze.js”,}npm run dev # 开发npm run build # 构建npm run analyze # 模块分析如果需要支持多入口构建,在命令后面添加参数:npm run dev – home # 开发 home 页面 npm run analyze – explore # 模块分析 explore 页面 # 构建多个页面 npm run build – home explore about/* about/all –env test/prod home, explore 确定构建的页面;about/, about/all 指 about 目录下所有的页面;all, * 整个项目所有的页面有时候可能还会针对不同的服务器环境(比如测试机、正式机)做出不同的构建,可以在后面加参数– 用来分割 npm 本身的参数与脚本参数,参考 npm - run-script 了解详情2.4 dev.js 配置开发一般用需要用到下面的组件:webpackwebpack-dev-server 或 webpack-dev-middlewarewebpack-hot-middlewareHotModuleReplacementPluginbrowser-syncconst minimist = require(‘minimist’);const webpack = require(‘webpack’);const HtmlWebpackPlugin = require(‘html-webpack-plugin’);const devMiddleWare = require(‘webpack-dev-middleware’);const hotMiddleWare = require(‘webpack-hot-middleware’);const browserSync = require(‘browser-sync’);const VueLoaderPlugin = require(‘vue-loader/lib/plugin’);const { HotModuleReplacementPlugin } = webpack;const argv = minimist(process.argv.slice(2));const page = argv.[0]; // 单页面const entryFile = ${__dirname}/src/index.js;// 多页面 const entryFile = ${__dirname}/src/${page}/index.js; // 编译器对象const compiler = webpack({ entry: [ ‘webpack-hot-middleware/client?reload=true’, // 热重载需要 entryFile, ], output: { path: ${__dirname}/dev/, // 打包到 dev 目录 filename: ‘index.js’, publicPath: ‘/dev/’, }, plugins: [ new HotModuleReplacementPlugin(), // 热重载插件 new HtmlWebpackPlugin({ // 处理 html // 单页面 template: ${__dirname}/src/index.html, // 多页面 template: ${__dirname}/src/${page}/index.html, }), new VueLoaderPlugin(), // vue-loader 所需 ], module: { rules: [ { // js 文件加载器 loader: ‘babel-loader’, exclude: /node_modules/, options: { presets: [’@babel/preset-env’, ‘@babel/preset-react’], plugins: [ ‘@babel/plugin-transform-react-jsx’, ‘@babel/plugin-syntax-dynamic-import’, ], }, test: /.(js|jsx)$/, }, { // css 文件加载器 loader: ‘style-loader!css-loader’, test: /.css$/, }, { // less 文件加载器 loader: ‘style-loader!css-loader!less-loader’, test: /.less$/, }, { // scss 文件加载器 loader: ‘style-loader!css-loader!sass-loader’, test: /.(scss|sass)$/, }, { // 静态文件加载器 loader: ‘url-loader’, test: /.(gif|jpg|png|woff|woff2|svg|eot|ttf|ico)$/, options: { limit: 1, }, }, { // html 文件加载器 loader: ‘html-loader’, test: /.html$/, options: { attrs: [‘img:src’, ’link:href’], interpolate: ‘require’, }, }, { // vue 文件加载器 loader: ‘vue-loader’, test: /.vue$/, }, ], }, resolve: { alias: {}, // js 配置别名 modules: [${__dirname}/src, ’node_modules’], // 模块寻址基路径 extensions: [’.js’, ‘.jsx’, ‘.vue’, ‘.json’], // 模块寻址扩展名 }, devtool: ’eval-source-map’, // sourcemap mode: ‘development’, // 指定 webpack 为开发模式});// browser-sync 配置const browserSyncConfig = { server: { baseDir: ${__dirname}/, // 静态服务器基路径,可以访问项目所有文件 }, startPath: ‘/dev/index.html’, // 开启服务器窗口时的默认地址};// 添加中间件browserSyncConfig.middleware = [ devMiddleWare(compiler, { stats: ’errors-only’, publicPath: ‘/dev/’, }), hotMiddleWare(compiler),];browserSync.init(browserSyncConfig); // 初始化浏览器实例,开始调试开发2.5 build.js 配置构建过程中,一般会有这些过程:提取样式文件,单独打包、压缩、添加浏览器厂家前缀对 js 在产品模式下进行打包,并生成 sourcemap 文件html-webpack-plugin 自动把打包好的样式文件与脚本文件引用到 html 文件中,并压缩对所有资源进行 hash 化处理(可选)const minimist = require(‘minimist’);const webpack = require(‘webpack’);const chalk = require(‘chalk’);const autoprefixer = require(‘autoprefixer’);const HtmlWebpackPlugin = require(‘html-webpack-plugin’);const MiniCssExtractPlugin = require(‘mini-css-extract-plugin’);const OptimizeCssAssetsPlugin = require(‘optimize-css-assets-webpack-plugin’);const VueLoaderPlugin = require(‘vue-loader/lib/plugin’);const {yellow, red} = chalk;const argv = minimist(process.argv.slice(2));const pages = argv.; // [‘home’, ’explore’, ‘about/’, ‘about/all’]const allPages = getAllPages(pages); // 根据 page 中的 *, all 等关键字,获取所有真正的 pages// 单页面,只有一个入口,所以只有一个配置文件const config = { … }; // 多页面,多个入口,所有有多个配置文件const configs = allPages.map(page => ({ // 单页面 entry: ${__dirname}/src/index.js, // js 入口文件 // 多页面 entry: ${__dirname}/src/${page}/index.js, // js 入口文件 output: { path: ${__dirname}/dist/, // 输出路径 filename: ‘[chunkhash].js’, // 输出文件名,这里完全取 hash 值来命名 hashDigestLength: 32, // hash 值长度 publicPath: ‘/dist/’, }, plugins: [ new MiniCssExtractPlugin({ // 提取所有的样式文件,单独打包 filename: ‘[chunkhash].css’, // 输出文件名,这里完全取 hash 值来命名 }), new HtmlWebpackPlugin({ // 单页面 template: ${__dirname}/src/index.html, // html 入口文件 // 多页面 template: ${__dirname}/src/${page}/index.html,// html 入口文件 minify: { // 指定如果压缩 html 文件 removeComments: !0, collapseWhitespace: !0, collapseBooleanAttributes: !0, removeEmptyAttributes: !0, removeScriptTypeAttributes: !0, removeStyleLinkTypeAttributes: !0, minifyJS: !0, minifyCSS: !0, }, }), new VueLoaderPlugin(), // vue-loader 所需 new OptimizeCssAssetsPlugin({ // 压缩 css cssProcessorPluginOptions: { preset: [‘default’, { discardComments: { removeAll: true } }], }, }), // webpack 打包的 js 文件是默认压缩的,所以这里不需要再额外添加 uglifyjs-webpack-plugin ], module: { rules: [ { // js 文件加载器,与 dev 一致 loader: ‘babel-loader’, exclude: /node_modules/, options: { presets: [’@babel/preset-env’, ‘@babel/preset-react’], plugins: [ ‘@babel/plugin-transform-react-jsx’, ‘@babel/plugin-syntax-dynamic-import’, ], }, test: /.(js|jsx)$/, }, { // css 文件加载器,添加了浏览器厂家前缀 use: [ MiniCssExtractPlugin.loader, ‘css-loader’, { loader: ‘postcss-loader’, options: { plugins: [ autoprefixer({ browsers: [ ‘> 1%’, ’last 2 versions’, ‘Android >= 3.2’, ‘Firefox >= 20’, ‘iOS 7’, ], }), ], }, }, ], test: /.css$/, }, { // less 文件加载器,添加了浏览器厂家前缀 use: [ MiniCssExtractPlugin.loader, ‘css-loader’, { loader: ‘postcss-loader’, options: { plugins: [ autoprefixer({ browsers: [ ‘> 1%’, ’last 2 versions’, ‘Android >= 3.2’, ‘Firefox >= 20’, ‘iOS 7’, ], }), ], }, }, ’less-loader’, ], test: /.less$/, }, { // scss 文件加载器,添加了浏览器厂家前缀 use: [ MiniCssExtractPlugin.loader, ‘css-loader’, { loader: ‘postcss-loader’, options: { plugins: [ autoprefixer({ browsers: [ ‘> 1%’, ’last 2 versions’, ‘Android >= 3.2’, ‘Firefox >= 20’, ‘iOS 7’, ], }), ], }, }, ‘sass-loader’, ], test: /.(scss|sass)$/, }, { // 静态文件加载器,与 dev 一致 loader: ‘url-loader’, test: /.(gif|jpg|png|woff|woff2|svg|eot|ttf|ico)$/, options: { limit: 1, }, }, { // html 文件加载器,与 dev 一致 loader: ‘html-loader’, test: /.html$/, options: { attrs: [‘img:src’, ’link:href’], interpolate: ‘require’, }, }, { // vue 文件加载器,与 dev 一致 loader: ‘vue-loader’, test: /.vue$/, }, ], }, resolve: { alias: {}, // js 配置别名 modules: [${__dirname}/src, ’node_modules’], // 模块寻址基路径 extensions: [’.js’, ‘.jsx’, ‘.vue’, ‘.json’], // 模块寻址扩展名 }, devtool: ‘source-map’, // sourcemap mode: ‘production’, // 指定 webpack 为产品模式}));// 执行一次 webpack 构建const run = (config, cb) => { webpack(config, (err, stats) => { if (err) { console.error(red(err.stack || err)); if (err.details) { console.error(red(err.details)); } process.exit(1); } const info = stats.toJson(); if (stats.hasErrors()) { info.errors.forEach(error => { console.error(red(error)); }); process.exit(1); } if (stats.hasWarnings()) { info.warnings.forEach(warning => { console.warn(yellow(warning)); }); } // 如果是多页面,需要把 index.html => ${page}.html // 因为每个页面导出的 html 文件都是 index.html 如果不重新命名,会被覆盖掉 if(cb) cb(); });};// 单页面run(config);// 多页面let index = 0;// go onconst goon = () => { run(configs[index], () => { index += 1; if (index < configs.length) goon(); });};goon();2.6 analyze.js 配置const minimist = require(‘minimist’);const chalk = require(‘chalk’);const webpack = require(‘webpack’);const { BundleAnalyzerPlugin } = require(‘webpack-bundle-analyzer’);const VueLoaderPlugin = require(‘vue-loader/lib/plugin’);const {yellow, red} = chalk;const argv = minimist(process.argv.slice(2));const page = argv._[0]; // 单页面const entryFile = ${__dirname}/src/index.js;// 多页面 const entryFile = ${__dirname}/src/${page}/index.js; const config = { entry: entryFile, output: { path: ${__dirname}/analyze/, // 打包到 analyze 目录 filename: ‘index.js’, }, plugins: [ new VueLoaderPlugin(), // vue-loader 所需 new BundleAnalyzerPlugin(), // 添加插件 ], module: { rules: [ { // js 文件加载器 loader: ‘babel-loader’, exclude: /node_modules/, options: { presets: [’@babel/preset-env’, ‘@babel/preset-react’], plugins: [ ‘@babel/plugin-transform-react-jsx’, ‘@babel/plugin-syntax-dynamic-import’, ], }, test: /.(js|jsx)$/, }, { // css 文件加载器 loader: ‘style-loader!css-loader’, test: /.css$/, }, { // less 文件加载器 loader: ‘style-loader!css-loader!less-loader’, test: /.less$/, }, { // scss 文件加载器 loader: ‘style-loader!css-loader!sass-loader’, test: /.(scss|sass)$/, }, { // 静态文件加载器 loader: ‘url-loader’, test: /.(gif|jpg|png|woff|woff2|svg|eot|ttf|ico)$/, options: { limit: 1, }, }, { // html 文件加载器 loader: ‘html-loader’, test: /.html$/, options: { attrs: [‘img:src’, ’link:href’], interpolate: ‘require’, }, }, { // vue 文件加载器 loader: ‘vue-loader’, test: /.vue$/, }, ], }, resolve: { alias: {}, // js 配置别名 modules: [${__dirname}/src, ’node_modules’], // 模块寻址基路径 extensions: [’.js’, ‘.jsx’, ‘.vue’, ‘.json’], // 模块寻址扩展名 }, mode: ‘production’, // 指定 webpack 为产品模式};webpack(config, (err, stats) => { if (err) { console.error(red(err.stack || err)); if (err.details) { console.error(red(err.details)); } process.exit(1); } const info = stats.toJson(); if (stats.hasErrors()) { info.errors.forEach(error => { console.error(red(error)); }); process.exit(1); } if (stats.hasWarnings()) { info.warnings.forEach(warning => { console.warn(yellow(warning)); }); }});2.7 扩展配置你可以根据需要扩展配置,比如添加插件、加载器等,比如:provide-plugin 可以提供一些全局模块的导出,比如 jquerydefine-plugin 可以动态定义一些全局变量css-loader 可以配置成 css-modules如果某个页面导出 js bundle 很大,想分割成多个文件,可以使用 dll-plugin、split-chunks-plugin如果想在命令行显示构建的进度,可以使用 progress-plugin3. 封装上面的代码可以封装成一个全局命令,比如 lila,运行上面的命令就可以更简洁:lila dev home # 开发 home 页面 lila analyze explore # 模块分析 explore 页面 # 构建多个页面 lila build home explore about/* about/all –env test/prod后续更多博客,查看 https://github.com/senntyou/blogs作者:深予之 (@senntyou)版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证) ...

December 4, 2018 · 7 min · jiezi

你想知道关于package-lock.json的一切,但是太害怕了问了?

简介如果你已经将节点包管理(npm)更新到版本5.x.x,看起来一切似乎都很顺利。等等,这是什么?用 npm 初始化项目的会自动创建了一个新文件 package-lock.json。如果打开它,它看起来有点像 package.json 的依赖项,但更冗长。我们决定忽略它,继续开发项目。最终,我们有时会遇到依赖项的问题,找不到,或者安装了错误的版本。大多数人最终都会删package-lock.json和运行“npm install”。那么,为什么要有它呢? 它应该做什么? 它实际上是做什么的?总结如果你使用的 npm 版本 为 ^5.x.x , package-lock.json 会默认自动生成你应该使用 package-lock 来确保一致的安装和兼容的依赖关系你应该将 package-lock 提交到源代码控制从npm ^ 5.1.x开始,package.json能够胜过 package-lock.json,所以你遇到较少让人头痛的问题不再删除 package-lock 只是为了运行npm install并重新生成它背景语义版本控制在你了解 package-lock 甚至 package.jso n之前,你必须了解语义版本控制(semver)。 这是npm背后的天才,是什么使它更成功。 你可以在 此处 阅读有关npm如何使用它的更多信息。简而言之,如果你正在构建与其他应用程序接口的应用程序,你应该告知你所做的更改将如何影响第三方与你的应用程序交互的能力。这是通过语义版本控制完成的,版本由三部分组成:X,Y,Z,分别是主要版本,次要版本和补丁版本。例如:1.2.3,主要版本1,次要版本2,补丁3。补丁中的更改表示不会破坏任何内容的错误修复。 次要版本的更改表示不会破坏任何内容的新功能。 主要版本的更改代表了一个破坏兼容性的大变化。 如果用户不适应主要版本更改,则内容将无法正常工作。管理包npm 存在使管理包变得容易。你的项目可能有数百个依赖项,每个依赖项都有一百个,为了让你的注意力远离依赖地狱,通过 npm 管理,使用一些简单的命令,你可以安装和管理这些依赖关系,几乎不必考虑它们。当您使用npm安装包(并保存它)时,会在 package.json 中添加一个包含包名称和应该使用的 semver的条目。默认情况下,npm 安装最新版本,并预先插入版本号,例如 “^1.2.12”,这表示至少应该使用版本 1.2.12,但任何高于此版本的版本都可以,只要它具有相同的主要版本,由于次要版本和补丁编号仅代表错误修正和非破坏性添加, 你可以安全地使用任何更高版本的同一主要版本。阅读更多关于semver通配符的信息,请看 这里。共享项目在 package.json 中定义这样的依赖项的真正好处是,任何有权访问 package.json 的人都可以创建一个包含运行应用程序所需模块的依赖项文件夹,但是让我们来看看事情可能出错的具体方式。假设我们创建了一个将使用 express 的新项目。 运行npm init后,我们安装express:npm install express - save。在编写代码时,最新的版本是4.15.4,所以 “express”:“^ 4.15.4”作为我的package.json中的依赖项添加,并且我的电脑安装了确切的版本。现在也许明天,express 的维护者会发布 bug 修复,因此最新版本变为4.15.5。 然后,如果有人想要为我的项目做贡献,他们会克隆它,然后运行`npm install。‘因为4.15.5是具有相同主要版本的更高版本,所以为它们安装。 我们都安装 express ,但我们却是不同的版本。从理论上讲,它们应该仍然是兼容的,但也许bugfix会影响我们正在使用的功能,而且当使用Express版本4.15.4和4.15.5运行时,我们的应用程序会产生不同的结果。Package-lock目的package-lock.json 的目的是避免上述情况,其中从同一 package.json 安装模块会导致两种不同的安装。 在 npm 版本 5.x.x 中添加了 package-lock.json,因此如果你使用的是主要版本 5 或更高版本,除非您禁用它,否则它会自动生成。内容结构package-lock 是 package.json 中列出的每个依赖项的大型列表,应安装的特定版本,模块的位置(URI),验证模块完整性的哈希,它需要的包列表 ,以及依赖项列表。 让我们来看看 express 的列表是什么:“express”: { “version”: “4.15.4”, “resolved”: “https://registry.npmjs.org/express/-/express-4.15.4.tgz", “integrity”: “sha1-Ay4iU0ic+PzgJma+yj0R7XotrtE=”, “requires”: { “accepts”: “1.3.3”, “array-flatten”: “1.1.1”, “content-disposition”: “0.5.2”, “content-type”: “1.0.2”, “cookie”: “0.3.1”, “cookie-signature”: “1.0.6”, “debug”: “2.6.8”, “depd”: “1.1.1”, “encodeurl”: “1.0.1”, “escape-html”: “1.0.3”, “etag”: “1.8.0”, “finalhandler”: “1.0.4”, “fresh”: “0.5.0”, “merge-descriptors”: “1.0.1”, “methods”: “1.1.2”, “on-finished”: “2.3.0”, “parseurl”: “1.3.1”, “path-to-regexp”: “0.1.7”, “proxy-addr”: “1.1.5”, “qs”: “6.5.0”, “range-parser”: “1.2.0”, “send”: “0.15.4”, “serve-static”: “1.12.4”, “setprototypeof”: “1.0.3”, “statuses”: “1.3.1”, “type-is”: “1.6.15”, “utils-merge”: “1.0.0”, “vary”: “1.1.1” } },可以在“requires”部分中列出的每个包中找到等效条目。npm(^5.x.x.x)后的做法,npm 使用package-lock.json,而不是使用 package.json 来解析和安装模块。因为 package-lock 为每个模块及其每个依赖项指定了版本,位置和完整性哈希,所以它每次创建的安装都是相同的。 无论你使用什么设备,或者将来安装它都无关紧要,每次都应该给你相同的结果,这非常有用。争议因此,如果引用 package-lock 是希望解决一个常见问题,为什么它的顶级搜索结果(除了npm文档)都是关于禁用它或质疑它扮演的角色?在npm 5.x.x之前,package.json 是项目的真实来源,npm 用户喜欢这个模型,并且非常习惯于维护他们的包文件。 但是,当首次引入 package-lock 时,它的行为与有多少人预期的相反。 给定一个预先存在的包和package-lock,对package.json的更改(许多用户认为是真实的来源)没有同步到package-lock 中。示例:包A,版本 1.0.0 在 package.json 和 package.lock.json 中。 在package.json中,A被手动编辑为1.1.0版。 如果认为 package.json 是真实来源的用户运行 npm install,他们会期望安装 1.1.0版。 但是,安装了1.0.0版,即使列出的 v1.1.0 是 package.json, 他们也希望安装是 1.0.0版。示例: package-lock.json 中不存在模块,但它存在于 package.json 中,作为一个将package.json 视为真实来源的用户,我希望能够安装我的模块。 但是,由于 package-lock.json 不存在该模块,因此未安装该模块,并且我的代码因无法找到模块而失败。大部分时间,因为我们无法弄清楚为什么我们的依赖关系没有被正确安装,要么删除了package-lock.json 并重新安装,要么完全禁用 package-lock.json 来解决问题。期望与真实行为之间的这种冲突在 npm repo中引发了一个非常有趣的问题线索。 有些人认为package.json 应该是事实的来源,有些人认为,因为 package-lock 是 npm 用来创建安装的东西,所以应该被认为是事实的来源。 这场争议的解决方案在于 PR#17508。 如果 package.json 已更新,Npm 维护者添加了一个更改,导致package.json 覆盖 package-lock。 现在,在上述两种情况下,都会正确安装用户期望安装的软件包。 此更改是作为npm v5.1.0的一部分发布的,该版本于2017年7月5日上线。你的点赞是我持续分享好东西的动力,欢迎点赞!一个笨笨的码农,我的世界只能终身学习!更多内容请关注公众号《大迁世界》 ...

December 3, 2018 · 2 min · jiezi

mpvue 单文件页面配置

前言mpvue 的出现把 vue 的开发体验带到了小程序这个平台中,但其目录结构与传统的 vue 项目却并不完全一致,一个典型的页面包含以下三个文件:index.vue // 页面文件main.js // 打包入口,完成 vue 的实例化main.json // 小程序特有的页面配置,早期写在 main.js 文件中其中,每个页面的 main.js 文件基本都是一致的,可通过 mpvue-entry 来自动生成(weex 也有类似的处理),而 main.json 我个人认为直接在 vue 文件中配置更为合适,于是开发了 mpvue-config-loader 来加以实现本文将介绍如何在 mpvue 官方模板的基础上,通过配置 mpvue-config-loader 来实现在 vue 文件内书写小程序的页面配置步骤初始化项目vue init mpvue/mpvue-quickstart my-project安装依赖npm i mpvue-config-loader -Doryarn add mpvue-config-loader -D修改打包配置build/webpack.base.conf.jsmodule.exports = { module: { rules: [ { test: /.vue$/, loader: ‘mpvue-loader’, options: vueLoaderConfig },+ {+ test: /.vue$/,+ loader: ‘mpvue-config-loader’,+ exclude: [resolve(‘src/components’)],+ options: {+ entry: ‘./main.js’+ }+ } … ] } … plugins: [ new MpvuePlugin(),- new CopyWebpackPlugin([{- from: ‘**/*.json’,- to: ‘’- }], {- context: ‘src/’- }), … ]}修改页面配置src/App.vue - 复制 app.json 中的内容,并修改格式以符合 eslint 规范<script>export default {+ config: {+ pages: [+ ‘pages/index/main’,+ ‘pages/logs/main’,+ ‘pages/counter/main’+ ],+ window: {+ backgroundTextStyle: ’light’,+ navigationBarBackgroundColor: ‘#fff’,+ navigationBarTitleText: ‘WeChat’,+ navigationBarTextStyle: ‘black’+ }+ }, created () { … }}src/pages/logs/index.vue - 同上import { formatTime } from ‘@/utils/index’import card from ‘@/components/card’export default {+ config: {+ navigationBarTitleText: ‘查看启动日志’+ }, …}src/app.json - 删除src/pages/logs/main.json - 删除启动运行npm run devoryarn dev其他使用 mpvue-entry 的项目暂不建议使用该模块,后期会直接集成作为可选模式之一该模块的实现方式有以下两种可选,但由于前者在编辑器中暂无法高亮,所以采用了第二种方式自定义标签 <config></config><script></script> 标签导出对象的 config 属性 ...

December 1, 2018 · 1 min · jiezi

浅探webpack优化

由于前端的快速发展,相关工具的发展速度也是相当迅猛,各大框架例如vue,react都有自己优秀的脚手架工具来帮助我们快速启动一个新项目,也正式因为这个原因,我们对于脚手架中最关键的一环webpack相关的优化知之甚少,脚手架基本上已经为我们做好了相关的开发准备,但是当我们想要做一些定制化的优化操作时,对webpack的优化也需要有一定的了解,否则无从下手,接下来就让我们进入webpack的优化世界构建速度提升loader提升loader是webpack中最重要的特性,由于webpack自身只支持JavaScript,因此需要一系列的loader来处理那些非JavaScript模块,因此在我们用webpack建项目的时候一定会使用一系列的loader,例如:vue-loader、sass-loader、babel-loader等等,就以babel-loader为例,来看具体配置:module: { rules: [{ test: /.js$/, exclude: /node_modules/, loader: ‘babel-loader?cacheDirectory=true’, options: { presets: [’@babel/preset-env’], plugins: [’@babel/transform-runtime’] } }] }对于loader来说最常用的就是exclude属性,用来避免不必要的转译,上面通过exclude来避免对node_modules中js中进行转译来提升构建速度,但是这样带来的提升效果有限。cacheDirectory是对babel-loader的转译结果进行缓存,之后的webpack进行构建时,都会去尝试读取缓存来避免高耗能的babel重新转译过程,cacheDirectory可以指定一个缓存目录或者指定为true,为true时将使用默认的缓存目录node_modules/.cache/babel-loader。babel对一些公共方法使用了非常小的辅助代码,默认会注入到每一个需要的文件,这样就造成重复引入,这时候就需要像上面那样引入transform-runtime来告诉babel引入runtime来代替注入第三方库优化externalsexternals提高构建速度的方法就是在构建时不会将指定的依赖包打包到bundle中,而是在运行时再从外部获取依赖,具体是怎么用的呢?来看个例子:externals : { vue : “Vue”, vueRouter : “VueRouter”, vueResource : “VueResource”, vuex : “Vuex”},<script type=“text/javascript” src=“https//xxxx/vue.famliy.1.1.0.min.js”></script>上面的例子的将vue全家桶都配置在externals中,然后将压缩包合成一个js文件放在cdn上面,这样就不会在构建时将文件打包到bundle中,提升打包速度,同时cdn又可以做缓存,提高访问速度,美滋滋DllPluginDllPlugin是用来干什么的呢?DllPlugin会将第三方包到一个单独文件,并且生成一个映射的json文件,打包的生成的文件就是一个依赖库,这个依赖不会随着你的业务代码改变而被重新打包,只有当它自身依赖的包发生变化时才会需要重新打包依赖库,接下来来看具体配置吧:module.exports = { entry: { vendor: [‘vue’, ‘vue-router’, ‘vue-resource’, ‘vuex’] }, output: { path: path.join(__dirname, ‘dist’), filename: ‘[name].js’, library: ‘[name]hash’, }, plugins: [ new webpack.DllPlugin({ name: ‘[name][hash]’, path: path.join(__dirname, ‘dist’, ‘[name]-manifest.json’), context: __dirname }) ]}首先我们需要一个如上面例子那样的dll配置文件,然后编译这个配置文件,生成一个vendor.js和一个映射文件vendor-manifest.json,然后再在我们的webpack配置文件中对进行配置:plugins: [ new webpack.DllReferencePlugin({ context: __dirname, manifest: require(’./dist/vendor-manifest.json’) }) ]这样就完成配置了,是不是很简单呢?赶紧动手试试吧happypackhappypack这是个什么呢?我们都知道webpack是个单线程处理任务的,当又多个任务需要处理的时候,需要排队,那happypack就是用多线程来处理任务,通过并发处理来提高任务处理速度,那么这个需要怎么配置呢?来看具体例子:const happypack = require(‘happypack’)// 创建并发池const threadPool = happypack.ThreadPool({size: os.cpus().length})module: { rules: [{ test: /.js$/, exclude: /node_modules/, loader: ‘happypack/loader?id=happyBabel’ // id对应happypack插件id }] },plugins: [ new happypack({ id: ‘happyBabel’, threadPool: threadPool, loaders: [‘babel-loader?cacheDirectory’] }) ],减小构建体积webpack-bundle-analyzer这个相信大家都很熟悉,就是一个可视化工具,用来查看各个包的大小以及相互之间的依赖关系,配置方法也很简单,就和插件的配置一样,来看具体例子:const bundleAnalyzerPlugin = require(‘webpack-bundle-analyzer’).BundleAnalyzerPluginplugins: [ new bundleAnalyzerPlugin() ],tree shakingtree shaking指的是什么呢?通常指的是JavaScript上下文中未引用的代码,怎么理解呢?比如你引用了lodash包,里面有许多和JavaScript相关的便利方法,但你实际只用了其中的一两个,此时打包时如果把所有的方法都打进去了,是不是很浪费呢?tree shaking的概念就是去除多余代码。来看一个简单的例子:import {plus} from ‘./count’console.log(plus(1, 2))function plus(x, y) { return x + y}function minus(x, y) { return x - y}export { plus, minus}const path = require(‘path’)module.exports = { entry: { main: ‘./src/index.js’, }, output: { path: path.join(__dirname, ‘dist’), filename: ‘[name].js’, }, mode: ‘development’}如上例所示,在入口文件中我们引入count.js中plus方法,我们期望的当然是只会引入plus方法,而不是都引入,但往往不随人愿,来看结果:你会发现编译后的代码中,整个count.js都被编译进去了,这时候你就需要tree shaking了,接下来看做tree shaking的具体方法UglifyJsPlugin这个插件大家一定都用过,使用UglifyJsPlugin就可以在构建的过程中对冗余的代码进行删除,在webpack4中只需要将上面mode的值改为production,就会启用UglifyJsPlugin,是不是很简单,或许你想知道webpack4中怎么自己配置UglifyJsPlugin,那就来看具体配置吧:const UglifyJsPlugin = require(‘uglifyjs-webpack-plugin’)optimization: { minimizer: [ new UglifyJsPlugin({ parallel: true, cache: true, uglifyOptions: { compress: { drop_console: true, reduce_vars: true }, output: { comments: false, beautify: false } } }) ] }是的在webpack4中的UglifyJsPlugin是配置在optimization中的minimizer中的,配置是不很简单呢?赶紧动手尝试吧按需加载(import)这里的import是指webpack中的动态加载,它的语法和ES6中的动态加载语法一摸一样,这是官方推荐的按需加载的方式,还是上面tree shaking的例子,我们只想引入plus方法,我们来看具体怎么使用:import(’./count.js’).then((count) => { console.log(count.plus(1, 2))})我们只需要将入口文件改成上面的形式,其他的都不要变就可以实现按需引入,是不是很简单呢?在vue中路由的按需加载也可以这么用,来看一个简单的例子:function view (name) { return new Promise((resolve, reject) => { import(’../views/’ + name + ‘.vue’) .then((res) => { resolve(res) }).catch(e => { reject(‘网络异常,请稍后再试’) }) }).catch(err => { throw new Error(’err,组件加载失败’) })}传入一个名字,动态引入对应目录的下的视图文件,这只是一个简单的例子,具体的使用形式还是依据具体的场景总结这篇文章简单的从构建速度和代码体积两个方面简单的介绍了webpack优化相关的方法,希望大家都能自己动手去写一写,毕竟只有实践出真知,更何况是编程。这篇文章如果有错误或不严谨的地方,欢迎批评指正,如果喜欢,欢迎点赞收藏 ...

December 1, 2018 · 2 min · jiezi

前端构建:3类13种热门工具的选型参考

前言在前端项目的规模和复杂性不断提升的情况下,各类构建思想和相应工具层出不穷。本文竭己所能对比了当下13个构建工具,包括Browserify、Webpack、Rollup、Grunt、Gulp和Yeoman6个广为流行的工具,FIS、Athena、WeFlow和Cooking等4个国产工具,以及三大框架:React,Vue和Angular的官方脚手架。希望能在项目初期的构建工具选型上为大家提供些参考。全览构建工具可以分为三类:模块化打包类、任务流构建类和集合型工具类(脚手架)。其中最为突出的,当属用于模块化打包的Webpack和用于任务流构建的Gulp。下面是截至2018年11月28日某时某刻,GitHub上各个工具的Star数目(听说Star数目可以造假?好生无聊的家伙们!)。前端的构建一般包括JS转码(使用Babel转ES6或TypeScript自转等)、CSS转码(Less或Sass转Css)、代码或资源的合并与压缩,基础检查和各类测试等等。这些虽与本文关系密切,但都不在讨论的范围之内。原因有二:一是实现这些功能的都是某些插件,不是工具本身,各类构建工具都是直接或间接(调用以自己的模式封装后的插件)使用它们的;二是本文介绍的是,构建方向上的类别和各类别里不同工具间的差异,与具体的操作无关。模块化打包类现在的前端项目基本是模块化的,原因就不在这多说。而模块化意味着分散,无法直接用于呈现,因此需要进行相应的打包形成一个整体。有些执行环境(Node)能自动打包各个模块,而有些(浏览器)则因为技术或其它考虑需要自行操作。模块化打包工具就是为模块化项目在浏览器上的优化呈现而服务的。模块化打包的核心是:找出依赖,打包成体。各类工具的基本运行思路便是根据已有配置,从某个文件开始,递归的找出所有与其相关的依赖模块,打包成某种类型的可直接在浏览器中运行的一个或多个新文件。这之中还可以优化输出,以实现代码分离、异步加载和长效缓存等高级功能。Browserify官网 | GitHub正如其官网介绍的,Browserify会递归的分析,项目中所有使用require引入的JS模块(包括Node内置模块)并打包,使得Node类项目能在浏览器上运行。不过对于与项目有关的其它资源,比如Css和图片等,依然需要手动管理。虽然网上已有人编写了支持此些功能的插件,但这不仅违背了设计初衷,也使配置变得杂乱。而且对于要求越来越高的单页面应用来说,它能提供的助力着实已显疲惫。Webpack官网 | 中文 | GitHub稳定版已到v4.26.0,本文以此版本为据。另附加官方的对比文档。Webpack的设计思想新颖实用,社区活跃,功能强大全面,已经是针对前端各类资源的、目前最优秀的模块化管理和打包工具。它入门简单,基本的常用功能能很快上手,并用于实际开发。但精通不易,毕竟打包已是web开发中最重要的挑战之一,必然要耗费些许精力。学习尚且不易,介绍就更为困难,得要有一本书的厚度。所幸此节不是详细介绍,只是亮点阐述,善哉善哉。入门已趋简单掌握了构建的基本思路,任意工具的入门都是较为简单的(读者批:废话)。之所以强调Webpack入门简单,是为了减轻有意者学习之前的顾虑。一方面是它刚被推出时,由于自身的概念新颖而且文档不全面,使开发者处于懵懵懂懂的状态,总感觉离真谛还差些距离。另一方面是它的体系着实庞大,仔细想想都不免胆怯。笔者初次接触时便是这些个感受。但现在不一样。吃土的日子已经远去,啃草的梦想还会远吗?大家准备好镰刀!Webpack第四版在入门上的方便性体现在三方面。一是基础功能高度集成和约定优于配置思想:安装好Webpack及其CLI后便可直接打包JS和JSON文件,与Browserify一样简单。二是官方文档详细(而且有基本同步的中文版),无论是概念的解析、实际运用的示例还是接口的展示都十分完备。三是现在使用和介绍Webpack的人已经很多了,因此网上的各路资料和相应问题的解决方案都十分丰富。你还在犹豫?一切皆模块如从官网上截取的图片所示,在Webapck眼中一切文件(.js、.css、.jpg、.woff、.csv和.ts等除了某些用于下载的静态大文件外)都是模块,都能通过与JS相似的方式被打包,并安置于合适浏览器渲染的位置。真是十分优秀的立足点。以此思想便可囊括前端会使用到的几乎所有资源,可以十分方便的统一管理和安置,更为便捷和高效。而且此思想就是为单页面应用而生的。在Webpack的另一层意境中,一个asset(各类资源)是一个模块,一个component是一个模块,一个module也是一个模块。而单页面应用的特点,不就是应用的更新不是整个页面的更新,而是某个module或component或asset的更新吗?十分的契合。有人说Webpack的缺点在服务端渲染(或说多页面应用)上。喂喂,一来别人的目标本就不在此,二是多页面应用也不需要如此复杂的支持体系。高效的构建性能单页面应用或说需要构建才能展示的应用,相比多页面应用,从每次修改到重新呈现要多经历一个构建的阶段。实际操作中,如果项目庞大而构建性能不够优化,一个小小的修改(打印某值)都会消耗5秒以上的时间,对开发者来说真是个地狱!而优化的方法不外乎两点,一是开发者优化项目的构建思路,二是构建工具优化自身的构建性能。Webpack拥有较理想的构建性能。在开发阶段,当开启了Webpack的模块热替换之后(使用webpack-dev-server会自动开启),一旦检测到文件被修改,会在应用程序运行过程中通过冒泡捕获的方式最小化替换、添加或删除模块,而无需重新加载整个页面。类似Dom渲染中的回流:如果子元素发生的大小变化,会影响兄弟元素但不影响父元素,那么父元素及其它是无需重新绘制的。而且即便完全重新构建,也会保留先前的应用程序状态,减少等待时间。活跃的社区活跃的社区可以提升系统的丰富度,降低学习与使用的成本。Webapck社区十分活跃,应用于各种需求的插件都被一一封装而可直接使用(官方也统一展示和说明了一些常用的优秀的Loader和Plugin)。不单单是其它工具的高度协调,开发中的各个阶段:搭建本地服务器、集成测试等,以及与任务流工具(Gulp、Grunt)的集成等等方面的解决或最优方案,都是丰富和全面的。基本上可以想到的需求,在这个社区中,都能直接借鉴他人已有的成果。Rollup官网 | 中文 | GitHubRollup定位为一个JS模块打包器(明指JS),主要用来构建JS库,也可服务于一些无需代码拆分和动态导入的小型应用程序。能在Webpack已稳居打包之首的情况下杀出一条血路,得到Vue、D3、Three和React等著名库的青睐,想必其着手点和性能有过人之处。Rollup本身结构简单,需要的配置项也不多,再加文档全面,所以很容易上手并全部掌握。它使用ES6本身的Module语法作为自己的标准,而不是诸如CommonJS和AMD等以前的解决方案。这意味着按照Module标准编成的代码,不仅现在可以借助Rollup打包运行,未来更能在实现此标准的浏览器上直接运行。通过Module的特性,Rollup开创了Tree-shaking功能——清除没有在项目中使用到的代码。它基于显式的import和export语句的方式,通过静态分析,排除了任何未在实际中使用的代码,能极大的减少构建于已有模块的项目体积。再加上其构建基本不添加自身的控制代码,使打包后的文件真正的达到纯净二字。想想还有点痒痒,我挠挠裆部。与 Webpack 对比Rollup和Webpack因其定位和专注点是可以共同存在并相互支持的。正如Rollup官网所说的,Rollup更适合构建独立的JS库,而Webpack为资源丰富的应用程序。虽然Webpack也增加了自己的Tree-shaking功能,但在编译后的输出代码中,简单地运行自动minifier检测未使用的变量,得到的结果是不如原生的静态分析。更何况Webpack生成的代码一定是经过自己包装后的代码——将每个模块封装在一个函数中,再置于一个包中,通过浏览器能使用的require方式逐一执行这些模块。任务流构建类基于任务的构建行为,是不在乎操作对象是否为模块化的。这类工具的目标是通过配置来解放日常需要重复的工作——转化、合并压缩和单元测试等等。有人说:这些操作Webpack和Rollup不是也能做?是的,基本能做。实际上,在用模块化构建工具的开发中,很少会用到任务流构建工具。但这绝不是说任务流工具会被取代,也不会被取代,至少多页面应用需要。再说任务流工具是十分纯粹的自动化行为,与模块化打包工具立足点就不一样,何谈取代一说。Grunt官网 | 中文 | GitHubGrunt虽是老牌构建工具,但依然被许多知名项目如WordPress、Twitter和Jquery等使用,也拥有持续更新的完整生态圈和中文文档。它是通过配置驱动——通过获取到的JSON配置执行操作,来流水线式执行相应任务。虽然在学习成本和执行效率上不出众,但如果项目原本就是通过它自动化构建的,是没有必要迁移到其它工具的。// Grunt 的配置驱动示例module.exports = function(grunt) { grunt.initConfig({ jshint: { files: [‘Gruntfile.js’, ‘src//*.js’, ’test//.js’], options: { globals: { jQuery: true } } }, watch: { files: [’<%= jshint.files %>’], tasks: [‘jshint’] } }); grunt.loadNpmTasks(‘grunt-contrib-jshint’); grunt.loadNpmTasks(‘grunt-contrib-watch’); grunt.registerTask(‘default’, [‘jshint’]);};Gulp官网 | 中文 | GitHubGulp是新型的构建工具,虽与Grunt的功能相同,但其构建过程却有三大优势。代码驱动代码驱动即通过执行实际代码驱动程序执行,与常见的配置驱动不同(Webpack、Rollup和Grunt等都是配置驱动)。从任务流构建的角度上看,代码驱动相比配置驱动有三点好处:一是高度的灵活;二是没有过多的配置项,减少学习成本;三是更方便错误的断定和异常情况的调试。// Gulp 的代码驱动示例gulp.src(’./client/templates/.jade’) .pipe(jade()) .pipe(gulp.dest(’./build/templates’)) .pipe(minify()) .pipe(gulp.dest(’./build/minified_templates’));Node流Gulp作为后来者,充分利用NodeJS流的思想进行IO操作,极大增加了大型项目的构建速度。比方说转化Scss成Css,Grunt的操作流程是:读取Scss文件、转化成Css、存储到磁盘,读取Css、压缩处理最后存储到磁盘;而Gulp得操作是:读取Scss文件、转化成Css、压缩处理最后存储到磁盘。一步到位,无需多次的IO操作。简单明了Gulp有十分精简的API。你能想到各种类型的任务,基本是通过仅有的五个可链式操作的方法实现的吗?不仅仅是学习和使用方便,编写后的功能也是一目了然。虽然代码驱动相比配置驱动,需要自己写的代码增加,但一是没增加难度只是函数名的多次重写,二是相对代码驱动的好处来说可以忽略。集合型工具类集合型工具类便是常说的脚手架,也可以看作是以模块化或任务流工具为主体的,各类常用工具的高度封装。它是一个开箱即可用的集合体,类似前后端同构时代的后端框架。它会根据你的选择,生成一个完整的、已配置好各类工具的、具有某些特定代码约定的项目框架。这些配置几乎包揽前端开发的整个流程,甚至可以集成自动化部署等后端接口。官方框架React CLI | Vue CLI | Angular CLI集合型工具一般为单页面应用服务,而单页面应用需要使用某个前端框架。无论你是用React、Vue或Angular,还是其它框架,首先得想到它是否有官方脚手架。比如Vue有Vue CLI。一般推荐有官方脚手架的直接使用官方的。因为现代前端框架一般不单独运行,需结合官方提供的其它工具,比如路由、状态管理等。而且各个框架及配件更新不断,每次更新都可能导致与其它插件的兼容问题,新的功能可能需要某些特定插件才能发挥作用。这是一项工程,仅靠个人或某些团体很难照顾周全的。而各个框架又都有意识的通过官方脚手架来充分展示新的特性,降低学习和使用的成本。我们何乐而不为呢?Yeoman官网 | GitHubYeoman是一个专为现代前端而生的、灵活通用的脚手架工具。它的运作方式和其它脚手架不同。在安装好CLI后,需要找到一个符合要求的Generator(一个npm包,相当于脚手架),使用Yeoman运行安装,生成初始化的项目。你也可以自行配置,使用Yeoman封装成符合特定需求的Generator,并发布出去。等到下次,其他人或你自己,需要生成符合此要求的项目时,便可以直接安装并使用Yeoman生成。这样有明显的两点好处:一是节省体力。在开始一个有特定需求的新项目时,如果有老项目可借鉴,一般会直接复制相关文件。但这样的复制文件可能不纯粹,即增加体积又带来安全隐患。二是在社区的支持下,很多有特殊要求的脚手架,早已有人解决并发布成Generator,是没必要自己动手的。国内其它百度 - FIS - 官网 | GitHub 微信 - WeFlow - 官网 | GitHub 京东 - Athena - 官网 | GitHub 饿了么 - Cooking(名字与公司的性质相得益彰) - 官网 | GitHub作为程序员或至各行各业,在与年龄增长速度相当的压力下,工资的高低自然成为日常性的评定标准。但在同行老友的酒桌上或某个太阳异常温煦下的小道上,能使自己为自己而不是其他事骄傲的,也肯定是“老子之前做过些什么”之类的实际付出而不是物质方面的获得。因此能够成为被公司支持的、被众多人使用的、开源框架维护团队中的程序员,多少是更为幸福的一类。这些由国内各个前端团队开发的集合型脚手架,都是基于自用在实践中得到的最为符合本身需求的产品。里面的包含内容十分丰富,不仅仅是这以上提到的前端本职工作,还有与后端的集成方案或自动化部署配置等。且流程简化,开箱即可使用。不过这些笔者都没用过,也没有打算用。不是打趣,原因很现实,有识之士可以在文章下留言。不用却依然写出的原因倒是简单:宣传,宣传即赞许和期盼;凑数,凑到13种好立个多少浮夸的标题。总结个人观点,不喜请喷,但要和蔼可亲。如果是使用某个前端框架开发应用程序,推荐框架官方的脚手架。如果是自己头脑发热想开源个JS库,推荐Rollup打包。如果不是模块化项目,又需要自动化处理一些事情,推荐Gulp作为构建工具。如果项目有特殊要求或作为核心的部件比较稀有,可以先查看Yeoman上是否有符合要求的Generator,没有就只能自食其力。最后如果你处在已有自己脚手架的公司(比如饿了么),可能要按规章制度使用Cooking为自己的仕途烹煮些吃食。肚子真饿,这种宣传饿了么会返优惠券吗?最后,如果是自食其力的搭建前人没有的脚手架,推荐使用Yeoman发布,方便你我他。 ...

November 30, 2018 · 1 min · jiezi

webpack学习笔记

1、什么是webpack模块打包工具 :分析项目结构,找到JS模块以及其它的一些浏览器不能直接运行的语言(less等),并将其打包为合适的格式以供浏览器使用。2、webpack核心概念主要有 6 部分:Entry : 输入入口,webpack构建第一步从这里开始Moudle :一个模块对应一个文件,从entry 开始递归找到所有依赖的模块Chunk:代码块,一个 Chunk 由多个模块组合而成,用于代码合并与分割Loader:模块转换器,将模块原内容按照需求转换成新内容Plugin:扩展插件,在 Webpack 构建流程中的特定时机注入扩展逻辑来改变构建结果或做你想要的事情Output:输出,在 Webpack 经过一系列处理并得出最终想要的代码后输出结果3、webpack 的工作流程项目当作一个整体,通过给定的主文件,webpack从主文件开始找到项目所有依赖的文件,再用loaders处理这些文件,最后打包为一个浏览器可识别的javascript文件。4、执行流程递归解析 entry 依赖的所有 module;每找到一个module,根据配置的loader去找相应的转换规则;对module进行转换后再解析当前module所依赖的module;这些模块以以恶搞entry为分组,以一个entry和依赖的module就是一个chunk;webpack将所有chunk 转换成文件输出,并在一定时候执行plugin逻辑。5、配置1、全局安装webpack$ sudo npm install -g webpack (全局安装)2、创建package.json文件夹$ npm init在终端中使用命令可以自动创建这个package.json文件。输入这个命令后,终端会问你一系列诸如项目名称,项目描述,作者等信息,不过不用担心,如果你不准备在npm中发布你的模块,这些问题的答案都不重要,回车默认即可3、在项目中安装Webpack作为依赖包npm install –save-dev webpack4、创建两个文件夹app文件夹和public文件夹,app文件夹用来存放原始数据和我们将写的JavaScript模块,public文件夹用来存放之后供浏览器读取的文件(包括使用webpack打包生成的js文件以及一个index.html文件)。接下来我们再创建三个文件:index.html –放在public文件夹中;Greeter.js– 放在app文件夹中;main.js– 放在app文件夹中;5、在index.html中写入代码目的在于引入打包后的js文件(这里我们先把之后打包后的js文件命名为bundle.js)6、在Greeter.js中写入代码定义一个返回包含问候信息的html元素的函数,并依据CommonJS规范导出这个函数为一个模块7、在main.js中写入代码用以把Greeter模块返回的节点插入页面8、通过配置文件使用webpack定义一个配置文件,将所有与打包相关信息均放在配置文件中。新建webpack.config.js文件,写入配置信息,主要涉及到的内容是入口文件路径和打包后文件的存放路径。有了这个配置之后,再打包文件,只需在终端里运行webpack(非全局安装需使用node_modules/.bin/webpack)命令就可以]9、更快捷的执行打包任务可以通过在package.json中对scripts对象进行相关设置即可,设置方法如下:![图片上传中…]配置后执行npm start即可进行快速打包10、webpack其他强大功能(1)source mapssource maps 提供编译文件和源文件的对应。配置后,webpack 就可以在打包时为我们生成source maps,这为我们提供了一种对应编译文件和源文件的方法,使得编译后的代码可读性更高,也更容易调试。在webpack的配置文件中配置source maps,需要配置 devtool:devtool选项配置结果source-map在一个单独的文件中产生一个完整且功能完全的文件。这个文件具有最好的source map,但是它会减慢打包速度;cheap-module-source-map在一个单独的文件中生成一个不带列映射的map,不带列映射提高了打包速度,但是也使得浏览器开发者工具只能对应到具体的行,不能对应到具体的列(符号),会对调试造成不便;eval-source-map使用eval打包源文件模块,在同一个文件中生成干净的完整的source map。这个选项可以在不影响构建速度的前提下生成完整的sourcemap,但是对打包后输出的JS文件的执行具有性能和安全的隐患。在开发阶段这是一个非常好的选项,在生产阶段则一定不要启用这个选项;cheap-module-eval-source-map这是在打包文件时最快的生成source map的方法,生成的Source Map 会和打包后的JavaScript文件同行显示,没有列映射,和eval-source-map选项具有相似的缺点;对小到中型的项目,eval-source-map 是一个很好的选项,只应该开发阶段使用它,继续对上文新建的webpack.config.js,进行如下配置:(2)构建本地服务器通过构建本地服务器,可以让浏览器监听代码修改,自动刷新修改后的结果,改构建基于node.js,不过它是一个单独的组件,在webpack中进行配置之前需要单独安装它作为项目依赖。npm install –save-dev webpack-dev-serverdev-server的配置项如下所示:devserver的配置选项功能描述contentBase默认webpack-dev-server会为根文件夹提供本地服务器,如果想为另外一个目录下的文件提供本地服务器,应该在这里设置其所在目录(本例设置到“public"目录)port设置默认监听端口,如果省略,默认为”8080“inline设置为true,当源文件改变时会自动刷新页面historyApiFallback在开发单页应用时非常有用,它依赖于HTML5 history API,如果设置为true,所有的跳转将指向index.html配置dev-server 位置在webpack.config.js中,配置添加后如下图所示:在配置好dev-server后还需要再package.json 的 scripts 中配置运行命令,如下图所示:(3)Loaders通过使用不同的loader,webpack有能力调用外部的脚本或工具,实现对不同格式的文件的处理,比如说分析转换scss为css,或者把下一代的JS文件(ES6,ES7)转换为现代浏览器兼容的JS文件,对React的开发而言,合适的Loaders可以把React的中用到的JSX文件转换为JS文件。Loaders 需要单独安装,且配置在webpack.config.js 中的 modules 关键字下进行配置,配置项:test:匹配 loaders 所处理文件的拓展名的正则表达式(必须)loader : loader 名称 (必须)include /exclude : 手动添加需要处理的文件(文件夹) 或 屏蔽不需要处理的文件(文件夹)(可选)query : 为loader提供额外的设置选项(可选)(4)Babelbabel 是一个js编译平台,可以编译ES6 、ES7,也可使用JSX等语法安装ES6以及JSX解析包// npm一次性安装多个依赖模块,模块之间用空格隔开 npm install –save-dev babel-core babel-loader babel-preset-env babel-preset-react在webpack中配置babel :(5)插件(Plugins)插件用于扩展webpack。plugins和loaders的区别:loaders 是在打包构建过程中用来处理源文件的(JSX,Scss,Less..),一次处理一个;plugins 插件并不直接操作单个文件,它直接对整个构建过程其作用。插件的使用需要用npm安装,然后在webpack配置中plugins字段下添加实例:打包后的文件就会添加版权声明。一些插件:Hot Module Replacement 允许你在修改组件代码后,自动刷新实时预览修改后的效果。HtmlWebpackPlugin 依据一个简单的index.html模板,生成一个自动引用你打包后的JS文件的新index.html问题记录:1、在安装时提示没有权限解决:mac 电脑有权限限制,需要使用 sudo 进行安装 ...

November 26, 2018 · 1 min · jiezi

vue-cli在webpack的配置文件探究

我们在构建一个vue项目的时候是不是一顿操作猛如虎啊?npm install vue-cli -gvue init webpack vue-demonpm installnpm run dev然后稍等一小会儿,咱们就能在浏览器看到了咱们的界面啦实际操作项目里面咱们可能需要修改一些配置项才能满足咱们自己项目的需求先看看vue项目初始化的文件目录结构:├─build ├─config ├─dist├─node_modules├─src│ ├─assets│ ├─components│ ├─router│ ├─App.vue│ ├─main.js├─static├─.babelrc├─.editorconfig├─.gitignore├─.postcssrc.js├─index.html├─package-lock.json├─package.json└─README.md来看看重要的文件,按我理解的顺序来啊:1.package.json项目作为一个大家庭,每个文件都各司其职。package.json来制定名单,需要哪些npm包来参与到项目中来,npm install命令根据这个配置文件增减来管理本地的安装包。这里面存放着我们项目的配置项,比如引入的依赖、项目作者、项目版本、项目编译启动的命令(放到script下面的)、还有就是dependencies和devDependencies、node和npm的信息、浏览器版本信息等等。这儿说一下这个dependencies和devDependencies我们在使用npm install 安装模块或插件的时候,有两种命令把他们写入到 package.json 文件里面去,比如:–save-dev 安装的 插件,被写入到 devDependencies 对象里面去–save 安装的 插件 ,被写入到 dependencies 对象里面去package.json 文件里面的 devDependencies 和 dependencies 对象有什么区别呢?devDependencies 里面的插件只用于开发环境,不用于生产环境dependencies 是需要发布到生产环境的。npm install 会安装两个依赖npm install packagename 只安装dependencies的依赖npm install packagename –dev 会安装devDependencies的依赖如果你只是单纯的使用这个包而不需要进行一些改动测试之类的,只安装dependencies而不安装devDependencies。执行:npm install –productionfile-loader和url-loader的区别:以图片为例,file-loader可对图片进行压缩,但是还是通过文件路径进行引入,当http请求增多时会降低页面性能,而url-loader通过设定limit参数,小于limit字节的图片会被转成base64的文件,大于limit字节的将进行图片压缩的操作。总而言之,url-loader是file-loader的上层封装。2、.postcssrc.js.postcssrc.js文件其实是postcss-loader包的一个配置,在webpack的旧版本可以直接在webpack.config.js中配置,现版本中postcss的文档示例独立出.postcssrc.js,里面写进去需要使用到的插件。module.exports = { “plugins”: { “postcss-import”: {}, “postcss-url”: {}, “autoprefixer”: {} }}关于这个postcss,去搜一下吧,这玩意儿我估摸着以后肯定会成为继scss和less后最火的一个css预处理咳咳,觉得自己英语能过关的去这儿看看 postcss咱们这还是有中文的,哈哈 postcss3、 .babelrc要了解这玩意儿,就内容超级多了,babel在vue项目里面就是为了把ES6的语法转换成ES5,废话不多说,咱们还是给链接把,笑哭了 babel官方网站 去这儿直接了解这个神奇的东东4、src内文件我们开发的代码都存放在src目录下,根据需要我们通常会再建一些文件夹。比如pages的文件夹,用来存放页面让components文件夹专门做好组件的工作;api文件夹,来封装请求的参数和方法;store文件夹,使用vuex来作为vue的状态管理工具,我也常叫它作前端的数据库等。assets文件:脚手架自动回放入一个图片在里面作为初始页面的logo。平常我们使用的时候会在里面建立js,css,img,fonts等文件夹,作为静态资源调用components文件夹:用来存放组件,合理地使用组件可以高效地实现复用等功能,从而更好地开发项目。一般情况下比如创建头部组件的时候,我们会新建一个header的文件夹,然后再新建一个header.vue的文件夹router文件夹:该文件夹下有一个叫index.js文件,用于实现页面的路由跳转,具体使用请点击→vue-router传送门App.vue:作为我们的主组件,可通过使用开放入口让其他的页面组件得以显示。main.js:作为我们的入口文件,主要作用是初始化vue实例并使用需要的插件,小型项目省略router时可放在该处5、其他文件.editorconfig:编辑器的配置文件.gitignore:忽略git提交的一个文件,配置之后提交时将不会加载忽略的文件index.html:页面入口,经过编译之后的代码将插入到这来。package.lock.json:锁定安装时的包的版本号,并且需要上传到git,以保证其他人在npm install时大家的依赖能保证一致README.md:可此填写项目介绍node_modules:根据package.json安装时候生成的的依赖(安装包).eslint相关文件,存放项目的编写格式规范,例如缩进,末尾分号,单双引号,对项目开发很有帮助,特别是大项目,规范所有人的开发,减少代码规范冲突config文件夹├─config │ ├─dev.env.js │ ├─index.js │ ├─prod.env.js1、config/dev.env.jsconfig内的文件其实是服务于build的,大部分是定义一个变量export出去’use strict’//采用严格模式const merge = require(‘webpack-merge’)const prodEnv = require(’./prod.env’)//webpack-merge提供了一个合并函数,它将数组和合并对象创建一个新对象。//如果遇到函数,它将执行它们,通过算法运行结果,然后再次将返回的值封装在函数中.这边将dev和prod进行合并module.exports = merge(prodEnv, { NODE_ENV: ‘“development”’})2、config/prod.env.js当开发是调取dev.env.js的开发环境配置,发布时调用prod.env.js的生产环境配置’use strict’module.exports = { NODE_ENV: ‘“production”’}3、config/index.js’use strict’const path = require(‘path’)module.exports = { dev: { // 开发环境下面的配置 assetsSubDirectory: ‘static’, //子目录,一般存放css,js,image等文件 assetsPublicPath: ‘/’, //根目录 proxyTable: {}, //可利用该属性解决跨域的问题(axios) host: ’localhost’, // 地址 port: 8080, //端口号设置,端口号占用出现问题可在此处修改 autoOpenBrowser: false, //是否在编译(输入命令行npm run dev)后打开http://localhost:8080/页面,以前配置为true,近些版本改为false,个人偏向习惯自动打开页面 errorOverlay: true, //浏览器错误提示 notifyOnErrors: true, //跨平台错误提示 poll: false, //使用文件系统(file system)获取文件改动的通知devServer.watchOptions devtool: ‘cheap-module-eval-source-map’,//增加调试,该属性为原始源代码(仅限行)不可在生产环境中使用 cacheBusting: true, //使缓存失效 cssSourceMap: true //代码压缩后进行调bug定位将非常困难,于是引入sourcemap记录压缩前后的位置信息记录,当产生错误时直接定位到未压缩前的位置,将大大的方便我们调试 }, build: { // 生产环境下面的配置 index: path.resolve(__dirname, ‘../dist/index.html’), //index编译后生成的位置和名字,根据需要改变后缀,比如index.php assetsRoot: path.resolve(__dirname, ‘../dist’), //编译后存放生成环境代码的位置 assetsSubDirectory: ‘static’, //js,css,images存放文件夹名 assetsPublicPath: ‘/’, //发布的根目录,通常本地打包dist后打开文件会报错,此处修改为./。如果是上线的文件,可根据文件存放位置进行更改路径 productionSourceMap: true, devtool: ‘#source-map’, productionGzip: false, //unit的gzip命令用来压缩文件,gzip模式下需要压缩的文件的扩展名有js和css productionGzipExtensions: [‘js’, ‘css’], bundleAnalyzerReport: process.env.npm_config_report }}build文件夹├─build │ ├─build.js │ ├─check-versions.js │ ├─utils.js │ ├─vue-loader.conf.js │ ├─webpack.base.conf.js │ ├─webpack.dev.conf.js │ ├─webpack.prod.conf.js 1、build/build.js’use strict’require(’./check-versions’)() //check-versions:调用检查版本的文件。加()代表直接调用该函数process.env.NODE_ENV = ‘production’ //设置当前是生产环境//下面定义常量引入插件const ora = require(‘ora’) //加载动画const rm = require(‘rimraf’) //删除文件const path = require(‘path’)const chalk = require(‘chalk’) //对文案输出的一个彩色设置const webpack = require(‘webpack’)const config = require(’../config’) //默认读取下面的index.js文件const webpackConfig = require(’./webpack.prod.conf’)//调用start的方法实现加载动画,优化用户体验const spinner = ora(‘building for production…’)spinner.start()//先删除dist文件再生成新文件,因为有时候会使用hash来命名,删除整个文件可避免冗余rm(path.join(config.build.assetsRoot, config.build.assetsSubDirectory), err => { if (err) throw err webpack(webpackConfig, (err, stats) => { spinner.stop() if (err) throw err process.stdout.write(stats.toString({ colors: true, modules: false, children: false, // 如果您使用的是ts-loader,将其设置为true将导致在构建期间出现TypeScript错误 chunks: false, chunkModules: false }) + ‘\n\n’) if (stats.hasErrors()) { console.log(chalk.red(‘构建失败并出现错误\n’)) process.exit(1) } console.log(chalk.cyan(‘构建成功.\n’)) console.log(chalk.yellow( ‘Tip: 构建的文件旨在通过HTTP服务器提供.\n’ + ‘正在打开index.html 在file:// won't work.\n’ )) })})2、build/check-version.js该文件用于检测node和npm的版本,实现版本依赖’use strict’const chalk = require(‘chalk’)const semver = require(‘semver’) //对版本进行检查const packageConfig = require(’../package.json’)const shell = require(‘shelljs’)function exec (cmd) { //返回通过child_process模块的新建子进程,执行 Unix 系统命令后转成没有空格的字符串 return require(‘child_process’).execSync(cmd).toString().trim()}const versionRequirements = [ { name: ’node’, currentVersion: semver.clean(process.version), //使用semver格式化版本 versionRequirement: packageConfig.engines.node //获取package.json中设置的node版本 }]if (shell.which(’npm’)) { versionRequirements.push({ name: ’npm’, currentVersion: exec(’npm –version’), // 自动调用npm –version命令,并且把参数返回给exec函数,从而获取纯净的版本号 versionRequirement: packageConfig.engines.npm })}module.exports = function () { const warnings = [] for (let i = 0; i < versionRequirements.length; i++) { const mod = versionRequirements[i] if (!semver.satisfies(mod.currentVersion, mod.versionRequirement)) { //上面这个判断就是如果版本号不符合package.json文件中指定的版本号,就执行下面错误提示的代码 warnings.push(mod.name + ‘: ’ + chalk.red(mod.currentVersion) + ’ should be ’ + chalk.green(mod.versionRequirement) ) } } if (warnings.length) { console.log(’’) console.log(chalk.yellow(‘To use this template, you must update following to modules:’)) console.log() for (let i = 0; i < warnings.length; i++) { const warning = warnings[i] console.log(’ ’ + warning) } console.log() process.exit(1) }}3、build/utils.jsutils是工具的意思,是一个用来处理css的文件。‘use strict’const path = require(‘path’)const config = require(’../config’)const ExtractTextPlugin = require(’extract-text-webpack-plugin’)const packageConfig = require(’../package.json’)//导出文件的位置,根据环境判断开发环境和生产环境,为config文件中index.js文件中定义的build.assetsSubDirectory或dev.assetsSubDirectoryexports.assetsPath = function (_path) { const assetsSubDirectory = process.env.NODE_ENV === ‘production’ ? config.build.assetsSubDirectory : config.dev.assetsSubDirectory //Node.js path 模块提供了一些用于处理文件路径的小工具 return path.posix.join(assetsSubDirectory, _path)}exports.cssLoaders = function (options) { options = options || {} //使用了css-loader和postcssLoader,通过options.usePostCSS属性来判断是否使用postcssLoader中压缩等方法 const cssLoader = { loader: ‘css-loader’, options: { sourceMap: options.sourceMap } } const postcssLoader = { loader: ‘postcss-loader’, options: { sourceMap: options.sourceMap } } // generate loader string to be used with extract text plugin function generateLoaders (loader, loaderOptions) { const loaders = options.usePostCSS ? [cssLoader, postcssLoader] : [cssLoader] if (loader) { loaders.push({ loader: loader + ‘-loader’, //Object.assign是es6语法的浅复制,后两者合并后复制完成赋值 options: Object.assign({}, loaderOptions, { sourceMap: options.sourceMap }) }) } //ExtractTextPlugin可提取出文本,代表首先使用上面处理的loaders,当未能正确引入时使用vue-style-loader if (options.extract) { return ExtractTextPlugin.extract({ use: loaders, fallback: ‘vue-style-loader’ }) } else { //返回vue-style-loader连接loaders的最终值 return [‘vue-style-loader’].concat(loaders) } } // https://vue-loader.vuejs.org/en/configurations/extract-css.html return { css: generateLoaders(), //需要css-loader 和 vue-style-loader postcss: generateLoaders(), //需要css-loader和postcssLoader 和 vue-style-loader less: generateLoaders(’less’), //需要less-loader 和 vue-style-loader sass: generateLoaders(‘sass’, { indentedSyntax: true }), //需要sass-loader 和 vue-style-loader scss: generateLoaders(‘sass’), //需要sass-loader 和 vue-style-loader stylus: generateLoaders(‘stylus’), //需要stylus-loader 和 vue-style-loader styl: generateLoaders(‘stylus’) //需要stylus-loader 和 vue-style-loader }}// Generate loaders for standalone style files (outside of .vue)exports.styleLoaders = function (options) { const output = [] const loaders = exports.cssLoaders(options) //将各种css,less,sass等综合在一起得出结果输出output for (const extension in loaders) { const loader = loaders[extension] output.push({ test: new RegExp(’\.’ + extension + ‘$’), use: loader }) } return output}exports.createNotifierCallback = () => { //发送跨平台通知系统 const notifier = require(’node-notifier’) return (severity, errors) => { if (severity !== ’error’) return //当报错时输出错误信息的标题,错误信息详情,副标题以及图标 const error = errors[0] const filename = error.file && error.file.split(’!’).pop() notifier.notify({ title: packageConfig.name, message: severity + ‘: ’ + error.name, subtitle: filename || ‘’, icon: path.join(__dirname, ’logo.png’) }) }}path.posix:提供对路径方法的POSIX(可移植性操作系统接口)特定实现的访问,即可跨平台,区别于win32。path.join:用于连接路径,会正确使用当前系统的路径分隔符,Unix系统是"/",Windows系统是"“4、vue-loader.conf.js该文件的主要作用就是处理.vue文件,解析这个文件中的每个语言块(template、script、style),转换成js可用的js模块。‘use strict’const utils = require(’./utils’)const config = require(’../config’)const isProduction = process.env.NODE_ENV === ‘production’const sourceMapEnabled = isProduction ? config.build.productionSourceMap : config.dev.cssSourceMap//处理项目中的css文件,生产环境和测试环境默认是打开sourceMap,而extract中的提取样式到单独文件只有在生产环境中才需要module.exports = { loaders: utils.cssLoaders({ sourceMap: sourceMapEnabled, extract: isProduction }), cssSourceMap: sourceMapEnabled, cacheBusting: config.dev.cacheBusting, // 在模版编译过程中,编译器可以将某些属性,如 src 路径,转换为require调用,以便目标资源可以由 webpack 处理. transformToRequire: { video: [‘src’, ‘poster’], source: ‘src’, img: ‘src’, image: ‘xlink:href’ }}5、webpack.base.conf.jswebpack.base.conf.js是开发和生产共同使用提出来的基础配置文件,主要实现配制入口,配置输出环境,配置模块resolve和插件等。‘use strict’const path = require(‘path’)const utils = require(’./utils’)const config = require(’../config’)const vueLoaderConfig = require(’./vue-loader.conf’)function resolve (dir) {//拼接出绝对路径 return path.join(__dirname, ‘..’, dir)}// 添加eslint过滤const createLintingRule = () => ({ test: /.(js|vue)$/, loader: ’eslint-loader’, enforce: ‘pre’, include: [resolve(‘src’), resolve(’test’)], options: { formatter: require(’eslint-friendly-formatter’), emitWarning: !config.dev.showEslintErrorsInOverlay }})module.exports = { //path.join将路径片段进行拼接,而path.resolve将以/开始的路径片段作为根目录,在此之前的路径将会被丢弃 //path.join(’/a’, ‘/b’) // ‘a/b’,path.resolve(’/a’, ‘/b’) // ‘/b’ context: path.resolve(__dirname, ‘../’), //配置入口,默认为单页面所以只有app一个入口 entry: { app: ‘./src/main.js’ }, //配置出口,默认是/dist作为目标文件夹的路径 output: { path: config.build.assetsRoot, filename: ‘[name].js’, publicPath: process.env.NODE_ENV === ‘production’ ? config.build.assetsPublicPath : config.dev.assetsPublicPath }, resolve: { //自动的扩展后缀,比如一个js文件,则引用时书写可不要写.js extensions: [’.js’, ‘.vue’, ‘.json’], //创建路径的别名,比如增加’components’: resolve(‘src/components’)等 alias: { ‘vue$’: ‘vue/dist/vue.esm.js’, ‘@’: resolve(‘src’), } }, //使用插件配置相应文件的处理方法 module: { rules: [ …(config.dev.useEslint ? [createLintingRule()] : []), // 加载eslint代码规范规则 //使用vue-loader将vue文件转化成js的模块 { test: /.vue$/, loader: ‘vue-loader’, options: vueLoaderConfig }, //js文件需要通过babel-loader进行编译成es5文件以及压缩等操作 { test: /.js$/, loader: ‘babel-loader’, include: [resolve(‘src’), resolve(’test’), resolve(’node_modules/webpack-dev-server/client’)] }, //图片、音像、字体都使用url-loader进行处理,超过10000会编译成base64 { test: /.(png|jpe?g|gif|svg)(?.)?$/, loader: ‘url-loader’, options: { limit: 10000, name: utils.assetsPath(‘img/[name].[hash:7].[ext]’) } }, { test: /.(mp4|webm|ogg|mp3|wav|flac|aac)(?.)?$/, loader: ‘url-loader’, options: { limit: 10000, name: utils.assetsPath(‘media/[name].[hash:7].[ext]’) } }, { test: /.(woff2?|eot|ttf|otf)(?.)?$/, loader: ‘url-loader’, options: { limit: 10000, name: utils.assetsPath(‘fonts/[name].[hash:7].[ext]’) } } ] },//以下选项是Node.js全局变量或模块,这里主要是防止webpack注入一些Node.js的东西到vue中 node: { // 防止webpack注入无用的setImmediate polyfill,因为Vue源包含它(虽然只有它是原生的才使用它) setImmediate: false, // 防止webpack将模拟注入到对客户端没有意义的Node本机模块 dgram: ’empty’, fs: ’empty’, net: ’empty’, tls: ’empty’, child_process: ’empty’ }}6、webpack.dev.conf.js’use strict’const utils = require(’./utils’)const webpack = require(‘webpack’)const config = require(’../config’)//通过webpack-merge实现webpack.dev.conf.js对wepack.base.config.js的继承const merge = require(‘webpack-merge’)const path = require(‘path’)const baseWebpackConfig = require(’./webpack.base.conf’)const CopyWebpackPlugin = require(‘copy-webpack-plugin’)const HtmlWebpackPlugin = require(‘html-webpack-plugin’)//美化webpack的错误信息和日志的插件const FriendlyErrorsPlugin = require(‘friendly-errors-webpack-plugin’)// 查看空闲端口位置,默认情况下搜索8000这个端口const portfinder = require(‘portfinder’)// processs为node的一个全局对象获取当前程序的环境变量,即hostconst HOST = process.env.HOSTconst PORT = process.env.PORT && Number(process.env.PORT)const devWebpackConfig = merge(baseWebpackConfig, { module: { //规则是工具utils中处理出来的styleLoaders,生成了css,less,postcss等规则 rules: utils.styleLoaders({ sourceMap: config.dev.cssSourceMap, usePostCSS: true }) }, //增强调试 devtool: config.dev.devtool, //此处的配置都是在config的index.js中设定好了 devServer: { clientLogLevel: ‘warning’, //控制台显示的选项有none, error, warning 或者 info //当使用 HTML5 History API 时,任意的 404 响应都可能需要被替代为 index.html historyApiFallback: { rewrites: [ { from: /./, to: path.posix.join(config.dev.assetsPublicPath, ‘index.html’) }, ], }, hot: true, //热加载 contentBase: false, // since we use CopyWebpackPlugin. compress: true, //压缩 host: HOST || config.dev.host, port: PORT || config.dev.port, open: config.dev.autoOpenBrowser, //调试时自动打开浏览器 overlay: config.dev.errorOverlay ? { warnings: false, errors: true } : false, // warning 和 error 都要显示 publicPath: config.dev.assetsPublicPath, proxy: config.dev.proxyTable, quiet: true, //控制台是否禁止打印警告和错误,若用FriendlyErrorsPlugin 此处为 true watchOptions: { poll: config.dev.poll, // 文件系统检测改动 } }, plugins: [ new webpack.DefinePlugin({ ‘process.env’: require(’../config/dev.env’) }), new webpack.HotModuleReplacementPlugin(), //模块热替换插件,修改模块时不需要刷新页面 new webpack.NamedModulesPlugin(), // 显示文件的正确名字 new webpack.NoEmitOnErrorsPlugin(), //当webpack编译错误的时候,来中端打包进程,防止错误代码打包到文件中 // https://github.com/ampedandwired/html-webpack-plugin // 该插件可自动生成一个 html5 文件或使用模板文件将编译好的代码注入进去 new HtmlWebpackPlugin({ filename: ‘index.html’, template: ‘index.html’, inject: true }), //复制插件 new CopyWebpackPlugin([ { from: path.resolve(__dirname, ‘../static’), to: config.dev.assetsSubDirectory, ignore: [’.*’] //忽略.的文件 } ]) ]})module.exports = new Promise((resolve, reject) => { portfinder.basePort = process.env.PORT || config.dev.port //查找端口号 portfinder.getPort((err, port) => { if (err) { reject(err) } else { //端口被占用时就重新设置evn和devServer的端口 process.env.PORT = port devWebpackConfig.devServer.port = port //友好地输出错误信息 devWebpackConfig.plugins.push(new FriendlyErrorsPlugin({ compilationSuccessInfo: { messages: [Your application is running here: http://${devWebpackConfig.devServer.host}:${port}], }, onErrors: config.dev.notifyOnErrors ? utils.createNotifierCallback() : undefined })) resolve(devWebpackConfig) } })})7、webpack.prod.conf.js’use strict’const path = require(‘path’)const utils = require(’./utils’)const webpack = require(‘webpack’)const config = require(’../config’)const merge = require(‘webpack-merge’)const baseWebpackConfig = require(’./webpack.base.conf’)const CopyWebpackPlugin = require(‘copy-webpack-plugin’)const HtmlWebpackPlugin = require(‘html-webpack-plugin’)const ExtractTextPlugin = require(’extract-text-webpack-plugin’)const OptimizeCSSPlugin = require(‘optimize-css-assets-webpack-plugin’)const UglifyJsPlugin = require(‘uglifyjs-webpack-plugin’)const env = require(’../config/prod.env’)const webpackConfig = merge(baseWebpackConfig, { module: { //调用utils.styleLoaders的方法 rules: utils.styleLoaders({ sourceMap: config.build.productionSourceMap, //开启调试的模式。默认为true extract: true, usePostCSS: true }) }, devtool: config.build.productionSourceMap ? config.build.devtool : false, output: { path: config.build.assetsRoot, filename: utils.assetsPath(‘js/[name].[chunkhash].js’), chunkFilename: utils.assetsPath(‘js/[id].[chunkhash].js’) }, plugins: [ // http://vuejs.github.io/vue-loader/en/workflow/production.html new webpack.DefinePlugin({ ‘process.env’: env }), new UglifyJsPlugin({ uglifyOptions: { compress: { warnings: false //警告:true保留警告,false不保留 } }, sourceMap: config.build.productionSourceMap, parallel: true }), //抽取文本。比如打包之后的index页面有style插入,就是这个插件抽取出来的,减少请求 new ExtractTextPlugin({ filename: utils.assetsPath(‘css/[name].[contenthash].css’), // 将以下选项设置为“false”将不会从codesplit块中提取CSS // 当webpack加载了codesplit块时,他们的CSS将使用style-loader动态插入 // 它目前设置为“true”,因为我们看到源代码包含在codesplit包中,当它是“false”增加文件大小时:https://github.com/vuejs-templates/webpack/issues/1110 allChunks: true, }), // 压缩提取的CSS。 我们正在使用此插件,以便可以减少来自不同组件的可能重复的CSS new OptimizeCSSPlugin({ cssProcessorOptions: config.build.productionSourceMap ? { safe: true, map: { inline: false } } : { safe: true } }), // 使用正确的资产哈希生成dist index.html以进行缓存. // 您可以通过编辑/index.html来自定义输出 // 请参阅https://github.com/ampedandwired/html-webpack-plugin new HtmlWebpackPlugin({ filename: config.build.index, template: ‘index.html’, inject: true, minify: { removeComments: true, //删除注释 collapseWhitespace: true, //删除空格 removeAttributeQuotes: true //删除属性的引号 // 更多属性参见:https://github.com/kangax/html-minifier#options-quick-reference }, // 通过CommonsChunkPlugin持续使用多个块的必要条件 chunksSortMode: ‘dependency’ //模块排序,按照我们需要的顺序排序 }), // 当供应商模块没有改变时,保持module.id稳定 new webpack.HashedModuleIdsPlugin(), // 启用域限制 new webpack.optimize.ModuleConcatenationPlugin(), // 将供应商js拆分为自己的文件 new webpack.optimize.CommonsChunkPlugin({ name: ‘vendor’, minChunks (module) { // node_modules中的任何必需模块都将解压缩到供应商 return ( module.resource && /.js$/.test(module.resource) && module.resource.indexOf( path.join(__dirname, ‘../node_modules’) ) === 0 ) } }), // 将webpack运行时和模块清单提取到其自己的文件中,以防止在更新应用程序包时更新供应商哈希 new webpack.optimize.CommonsChunkPlugin({ name: ‘manifest’, minChunks: Infinity }), // 此实例从代码拆分块中提取共享块,并将它们捆绑在一个单独的块中,类似于供应商块,请参阅:https://webpack.js.org/plugins/commons-chunk-plugin/#extra-async-commons-chunk new webpack.optimize.CommonsChunkPlugin({ name: ‘app’, async: ‘vendor-async’, children: true, minChunks: 3 }), // 复制自定义静态资源到dist里面 new CopyWebpackPlugin([ { from: path.resolve(__dirname, ‘../static’), to: config.build.assetsSubDirectory, ignore: [’.’] } ]) ]})if (config.build.productionGzip) { const CompressionWebpackPlugin = require(‘compression-webpack-plugin’) webpackConfig.plugins.push( new CompressionWebpackPlugin({ asset: ‘[path].gz[query]’, algorithm: ‘gzip’, test: new RegExp( ‘\.(’ + config.build.productionGzipExtensions.join(’|’) + ‘)$’ ), threshold: 10240, minRatio: 0.8 }) )}if (config.build.bundleAnalyzerReport) { const BundleAnalyzerPlugin = require(‘webpack-bundle-analyzer’).BundleAnalyzerPlugin webpackConfig.plugins.push(new BundleAnalyzerPlugin())}module.exports = webpackConfig全是代码,代码全是项目模块的东西。估计有好多小伙伴儿看一点点就关了,能看完的,给你比心哦,哈哈哈可以参考那本书叫《深入浅出webpack》 ...

November 23, 2018 · 7 min · jiezi

npm包的发布和管理

npm包管理npm其实是Node.js的包管理工具(node package manager)。为啥我们需要一个包管理工具呢?因为我们在Node.js上开发时,会用到很多别人写的JavaScript代码。如果我们要使用别人写的某个包,每次都根据名称搜索一下官方网站,下载代码,解压,再使用,非常繁琐。于是一个集中管理的工具应运而生:大家都把自己开发的模块打包后放到npm官网上,如果要使用,直接通过npm安装就可以直接用,不用管代码存在哪,应该从哪下载。更重要的是,如果我们要使用模块A,而模块A又依赖于模块B,模块B又依赖于模块C和模块D,npm可以根据依赖关系,把所有依赖的包都下载下来并管理起来。否则,靠我们自己手动管理,肯定既麻烦又容易出错。npm的基础使用npm的指令其实常用的并不多官方文档;列出来如下面:access Set access level on published packagesadduserAdd a registry user accountauditRun a security auditbinDisplay npm bin folderbugs Bugs for a package in a web browser maybebuildBuild a packagebundleREMOVED 已删除cacheManipulates packages cacheciInstall a project with a clean slatecompletion Tab Completion for npmconfigManage the npm configuration filesdedupeReduce duplicationdeprecateDeprecate a version of a packagedist-tagModify package distribution tagsdocsDocs for a package in a web browser maybedoctorCheck your environmentseditEdit an installed packageexploreBrowse an installed packagehelp-searchSearch npm help documentationhelpGet help on npmhookManage registry hooksinitcreate a package.json fileinstall-ci-testInstall a project with a clean slate and run testsinstall-testInstall package(s) and run testsinstallInstall a packagelinkSymlink a package folderlogout Log out of the registrylsList installed packagesnpm javascript package manageroutdatedCheck for outdated packagesowner Manage package ownerspackCreate a tarball from a packagepingPing npm registryprefixDisplay prefixprofileChange settings on your registry profilepruneRemove extraneous packagespublish Publish a packagerebuildRebuild a packagerepo Open package repository page in the browserrestartRestart a packageroot Display npm rootrun-scriptRun arbitrary package scriptssearch Search for packagesshrinkwrapLock down dependency versions for publicationstarMark your favorite packagesstarsView packages marked as favoritesstartStart a packagestopStop a packageteamManage organization teams and team membershipstestTest a packagetokenManage your authentication tokensuninstallRemove a packageunpublishRemove a package from the registryupdateUpdate a packageversion Bump a package versionviewView registry infowhoamiDisplay npm usernameinit初始化创建package.jsonnpm init [–force|-f|–yes|-y|–scope]npm init <@scope> (same as npx <@scope>/create)npm init [<@scope>/]<name> (same as npx [<@scope>/]create-<name>)search搜索查看远程npm相关资源包信息npm search [-l|–long] [–json] [–parseable] [–no-description] [search terms …]aliases: s, se, findinstall可以是说是install是最为常见的命令官方介绍,npm install (with no args, in package dir)npm install [<@scope>/]<name>npm install [<@scope>/]<name>@<tag>npm install [<@scope>/]<name>@<version>npm install [<@scope>/]<name>@<version range>npm install <git-host>:<git-user>/<repo-name>npm install <git repo url>npm install <tarball file>npm install <tarball url>npm install <folder> alias: npm icommon options: [-P|–save-prod|-D|–save-dev|-O|–save-optional] [-E|–save-exact] [-B|–save-bundle] [–no-save] [–dry-run] In global mode (ie, with -g or –global appended to the command), it installs the current package context (ie, the current working directory) as a global package. The -g or –global argument will cause npm to install the package globally rather than locally. The -f or –force argument will force npm to fetch remote resources even if a local copy exists on disk.上面的还介绍已经很详细了,所以这里只是讲一下npm install packageName [|–save |–save-prod|–save-dev]的区别;npm install babel npm5以前,会把X包安装到node_modules目录中,不会修改package.json的dependencies字段,之后运行npm install命令时,不会自动安装Xnpm install babelnpm5以后,会把X包安装到node_modules目录中,会修改package.json的dependencies字段,之后运行npm install命令时,会自动安装X, 线上环境时会被安装npm install babel -P -P, –save-prod: Package will appear in your dependencies. This is the default unless -D or -O are present. Package will appear in your dependencies, With the –production flag (or when the NODE_ENV environment variable is set to production), npm will install modules listed in dependencies.npm install babel -DPackage will appear in your devDependencies,With the –production flag (or when the NODE_ENV environment variable is set to production), npm will not install modules listed in devDependencies. 会把X包安装到node_modules目录中,会在package.json的devDependencies属性下添加X,之后运行npm install命令时,会自动安装X到node_modules目录中,之后运行npm install –production或者注明NODE_ENV变量值为production时,不会自动安装X到node_modules目录中update升级某个资源包或者全部资源包到某一个版本或者匹配的最新版本。npm update [-g] [<pkg>…]aliases: up, upgradeuninstall移除某个资源包npm uninstall [<@scope>/]<pkg>[@<version>]… [-S|–save|-D|–save-dev|-O|–save-optional|–no-save]aliases: remove, rm, r, un, unlinknpm包创建、编写、测试、维护Node出现之前,JavaScript是缺少包结构的。CommonJS致力于改变这种现状,于是定义了包的结构规范。而NPM的出现则是为了在CommonJS规范的基础上,实现解决包的安装卸载,依赖管理,版本管理等问题。require的查找机制明了之后,我们来看一下包的细节。一个符合CommonJS规范的包应该是如下这种结构:一个package.json文件应该存在于包顶级目录下二进制文件应该包含在bin目录下(可选)JavaScript代码入库是index.js,其他包含在lib目录下文档应该在doc目录下(可选)单元测试应该在test目录下(可选)初始化包创建包的根目录mkdir testpackage初始化npm init // 需要进行一些基本配置编写创建入口文件touch index.js编写代码const updateQueryString = function(url, key, value) { let urlParts = url.split(’#’), hash = ‘’, uri = urlParts.shift(), re = new RegExp(([?&amp;])${key}=.*?(&amp;|$), ‘i’), separator = uri.indexOf(’?’) !== -1 ? ‘&’ : ‘?’, encodeKey = encodeURIComponent(key), encodeValue = encodeURIComponent(value); urlParts.length > 0 && (hash = #${urlParts.join('#')}); if (uri.match(re)) { return uri.replace(re, $1${encodeKey}=${encodeValue}$2) + hash; } else { return ${uri}${separator}${encodeKey}=${encodeValue}${hash}; }};// 最后的导出部分module.exports = { updateQueryString};测试创建包的根目录npm i mocha -D // 安装测试库npm i chai -D // 安装断言库mkdir testcd testtouch index.test.js编写测试代码const utils = require(’./../index.js’);const expect = require(‘chai’).expect;let { updateQueryString} = utils;describe(‘updateQueryString函数的测试’, function() { it(‘https://test.com/path?test=11 修改test参数为22 应该等于 https://test.com/path?test=22', function() { expect(updateQueryString(‘https://test.com/path?test=11', ’test’, 22)).to.be.equal(‘https://test.com/path?test=22'); });});运行测试cd …/node_modules/mocha/bin/mocha npm包的发布注册账号npm官网终端执行 npm login,输入用户名和密码 、邮箱npm publish 发布Organization包我们经常可以看到@angular、@ionic他们的包, 都可以以@开头,那么我们的可不可以,原来angular、ionic都属于一个组织(Organization)只有新创建一个Organization组织之后,才能创建@testorg/testpackname这样的包!!!那么我们就可以去官网上创建我们的Organization,命名之后,官方步骤,初始化npm init –scope=<your_org_name>npm init foo -> npx create-foonpm init @usr/foo -> npx @usr/create-foonpm init @usr -> npx @usr/create修改package.json里面的name字段为@your_org_name/<pkg_name>发布npm publish –access public // 公开包发布npm包支持esmodule ...

November 22, 2018 · 3 min · jiezi

webpack4配置之分享几个常用插件

前言 继上一次webpack的基础配置分享之后,本次将分享一些工作中项目常用的配置插件、也会包含一些自己了解过觉得不错的插件,如有分析不到位的,欢迎纠错,嗯,这些东西文档都有,大佬可绕过。 Wepack4之后废弃了很多的插件,这里只会讲解webpack4还支持的(包含4之前插件),已经废弃的将不再阐述。 上一次的分享之后,有部分掘金网友留言质疑:骗小白的赞、是否原创、是否是抄别人等等,当然也有很多的网友支持和鼓励,不管褒贬,苏南都非常的感谢,是你们让我认识到自己的不足与优劣。 大家的留言,让我想起了自己刚入门前端初期的心酸,基本靠自己自学,没有人带,遇到问题像无头的苍蝇,到处乱撞网上一顿搜索,百度不曾欺我,在点了一个又一个的广告之后,翻过十页八页之后终于找到了问题的解决方案。 执着于对前端的热爱,常常一个问题卡到深夜,初入前端的我曾一度感叹在编辑器写一些东西,在网页上就能跑,甚至感叹 js 写上一个 alert hello world,浏览器就会自动弹出一个窗口,感觉全世界都在向你招手,当时的兴奋是难以形容的,甚至幻想着未来有一天,可能有千万、亿万的用户,在用你写的东西。 这几天一直在想,这篇插件的总结还是否要继续写下去?从写博客到今天,将近两个月吧,也算是一个新人,技术方面虽说工作了几年,但也不敢说多牛B,写博客的初衷是为了对自己工作中遇到的问题/心得等做一个总结,俗话说:好记性不如烂笔头;同时也希望能把自己遇到的问题、坑点分享给他人,让遇到同样问题的基友们能少走那么一点点弯路。 终于最后在想了很久之后明白,人无完人,百人百性、千人千面,不管你做总会有不同的声音,同样不管你分享什么,总会有人需要。所以走自己的路,让别人打车吧,坚持自己所想 努力成为自己想成为的样子,就是对自己最大的肯定 ———— 至曾经初入前端的我们。去做想做的事,去爱值得的人;去成为自己喜欢的模样,去让自己发光!浑身充满力量,充实的日子最美好!各位早安,这里是@IT·平头哥联盟,我是首席填坑官∙苏南,用心分享 一起成长 做有温度的攻城狮。公众号:honeyBadger8,群:912594095mini-css-extract-plugincss-提取,看名字就懂提取css用的。在这之前我们可能会使用extract-text-webpack-plugin比较多一些,两者相比它有什么优势呢?extract-text-webpack-plugin 它对css的提取,最终是根据你创建的实例、或者配置多个入口 chunk来的,比如你配置了一个入口文件,最终所有的css都会提取在一个样式文件里,如果你创建了多个extract-text-webpack-plugin实例,则会生成多个css的文件,而mini-css-extract-plugin,它默认就会对你的样式进行模块化拆分,嗯,有点跟output里的配置一个意思,异步按需加载,不再仅仅是js的特权;//extract-text-webpack-plugin 编译打包config.module.rules.push({ test: /.(scss|css)$/, use: ExtractTextPlugin.extract({ use: [ “css-loader”, { //首席填坑官∙苏南的专栏 交流:912594095、公众号:honeyBadger8 loader: ‘postcss-loader’, options: { plugins: [ require(‘autoprefixer’)({ //添加前缀 browsers: CSS_BROWSERS, }), ], minimize: true }, }, “sass-loader” ] })})config.plugins.push(new ExtractTextPlugin({ filename: ‘css/[name].css’, disable: false, allChunks: true,}));//mini-css-extract-plugin 编译打包config.module.rules.push({ test: /.(scss|css)$/,//同时处理css/scss use: [ { loader: MiniCssExtractPlugin.loader, }, “css-loader”, //css处理器 { loader: ‘postcss-loader’, /* postcss 这个插件的作用在于,与已有的工具集成一起使用,很少有单独使用的情况, 通用我们用的最多的,是配合 autoprefixer 来添加各浏览器的前缀,以达到更好的兼容, 再深入一些就是 cssnext 就是允许开发者自定义属性和变量 : color:var(–theme-color,#42b983); / options: { plugins: [ require(‘autoprefixer’)({ browsers: CSS_BROWSERS, }), ], }, }, “sass-loader” //sass处理器 、甚至还可以再加一个less的处理器 ]})config.plugins.push(new MiniCssExtractPlugin({ filename: ‘css/[name].css’, //这里配置跟output写法一致 chunkFilename: ‘css/[id][chunkhash:8].css’,}));config.plugins.push(new OptimizeCssAssetsPlugin({})); //压缩文件optimize-css-assets-webpack-plugin上面的示例里已经用到了,它的作用在于压缩css文件,assetNameRegExp:默认是全部的css都会压缩,该字段可以进行指定某些要处理的文件,cssProcessor:指定一个优化css的处理器,默认cssnano,cssProcessorPluginOptions:cssProcessor后面可以跟一个process方法,会返回一个promise对象,而cssProcessorPluginOptions就是一个options参数选项!canPrint:布尔,是否要将编译的消息显示在控制台,没发现有什么用!坑点 :建议使用高版本的包,之前低版本有遇到样式丢失把各浏览器前缀干掉的问题,new OptimizeCssAssetsPlugin({ assetNameRegExp: /.optimize.css$/g, cssProcessor: require(‘cssnano’), cssProcessorPluginOptions: { preset: [‘default’, { discardComments: { removeAll: true } }], //autoprefixer: { browsers: CSS_BROWSERS }, 也是可以指定前缀的 }, canPrint: true})SplitChunksPlugin、RuntimeChunkPlugin它们跟上一篇的optimization配置下的的splitChunks、runtimeChunk基本是一致的,;SplitChunksPlugin、RuntimeChunkPlugin,其实就是webpack4之前CommonsChunkPlugin的替代品,用于提取一些公共模块;chunks:要进行处理的类型,它有三个值:all,async,initialminSize:最少大小maxSize:最大包的大小,超出生成新的包minChunks:至少要引用N次的模块,maxAsyncRequests:最大的按需加载并行请求数量maxInitialRequests:最大的初始化加载请求次数new webpack.optimize.SplitChunksPlugin({ chunks: ‘async’, minSize: 30000, maxSize: 0, minChunks: 1, maxAsyncRequests: 1, maxInitialRequests:1, name: true, //可以指定 ……, }), new webpack.optimize.RuntimeChunkPlugin({ name: ‘manifest’, name: entrypoint => runtimechunk~${entrypoint.name} //动态文件名 })HotModuleReplacementPlugin热更新替换,在不刷新重载页面的情况下更换编辑修改后的代码:它只会更新改动过的内容,所以速度很快,几乎在自己刚改完,切到浏览器窗口内容就已经更新完了;使用 HotModuleReplacementPlugin插件后,它会暴露一个module.hot对象,它下面有很多的属性:accept:它有两个参数,一个是授权模块(可以直接是单个文件路径、也可以是一个数组包含多个文件路径),第二个参数,是回调函数,即更新后要做的逻辑处理。decline 有点黑名单的意思,就是忽略一些模块,不更新它们,status 当前更新的状态,idle、check、prepare、ready、dispose、apply、fail等;一般只用到 accept 最多,下面有个示例;new webpack.HotModuleReplacementPlugin()//路由入口页……if (module.hot) { module .hot .accept([ ‘./pages/routes’ ], (err) => { const NextRoute = require(’./pages/routes’) // 从DOM 中移除已经挂载的 React 组件 然后重装 ReactDOM.unmountComponentAtNode(APP_DOM); ReactDOM.render( <Provider store={Store}> <Router routes={NextRoute} history={browserHistory}/> </Provider>, APP_DOM); });}……html-webpack-plugin这个插件相信大家都熟悉的不能再熟悉了,把编译后的文件(css/js)插入到入口文件中,可以只指定某些文件插入,可以对html进行压缩等filename:输出文件名;template:模板文件,不局限于html后缀哦;removeComments:移除HTML中的注释;collapseWhitespace:删除空白符与换行符,整个文件会压成一行;inlineSource:插入到html的css、js文件都要内联,即不是以link、script的形式引入;inject:是否能注入内容到 输出 的页面去;chunks:指定插入某些模块;hash:每次会在插入的文件后面加上hash ,用于处理缓存,如:<link href="/css/index.css?v=a6fc12dd5002c">;其他:favicon、meta、title ……;new HtmlWebPackPlugin({ filename: path.resolve(__dirname, ‘../assets/index.html’), template: path.resolve(__dirname,"../views/temp.html"), minify:{ //压缩HTML文件 removeComments:true, collapseWhitespace:true }, inlineSource: ‘.(js|css)’, inject: false, chunks: [‘vendors’, ‘index’], //首席填坑官∙苏南的专栏 hash:true, favicon、meta、title等都可以配置,页面内使用「<%= htmlWebpackPlugin.options.title %>」即可 ……})uglifyjs-webpack-pluginjs代码压缩,默认会使用 optimization.minimizer,cache: Boolean/String ,字符串即是缓存文件存放的路径;test:正则表达式、字符串、数组都可以,用于只匹配某些文件,如:/.js(?.)?$/i;parallel : 启用多线程并行运行来提高编译速度,经常编译的时候听到电脑跑的呼呼响,可能就是它干的,哈哈~;output.comments : 删除所有注释,compress.warnings :插件在进行删除一些无用代码的时候,不提示警告,compress.drop_console:喜欢打console的同学,它能自动帮你过滤掉,再也不用担心线上还打印日志了;等等还有一些如:定义压缩的程度、提出多次出现但没有变量的值的配置,想深入的同学可移步官方;//默认:optimization:{ minimizer:true};//自定义minimizer: [ new UglifyJsPlugin({ cache: true, // cache: “assets”, parallel: true, //也可以指定 Number ,即最多并行运行数量 sourceMap: true, uglifyOptions: { output: { comments: false, …… //首席填坑官∙苏南的专栏,QQ:912594095 }, compress: { warnings: false, drop_console:true, …… } }, }),],BannerPlugin这个插件,它的作用在于某些时候,我们需要对文件添加一些说明,比如版本号,作者、日期等,它就可以帮到,每次编译,在头部插件一些注释;它可以直接是一个字符串,也可以是一个options;嗯,差点忘说了,它是webpack自带的一个插件,不用另外再安装依赖,//字符串:new webpack.BannerPlugin(‘给文件添加一些信息,打包日期:’+ new Date());//自定义plugins: [ new webpack.BannerPlugin({ { banner: ’ \n @item:苏南的项目 \n @author:suSouth \n @date:’+new Date()+’ \n @description:苏南的项目 \n @version:’+package.version+’ \n’, // 要输出的注释内容 test:string/正则/数组,//可用于匹配某些文件才输出, entryOnly: boolean, // 即是否只在入口 模块 文件中添加注释; …… } })],preload-webpack-plugin在使用这个插件之前,我们需要先了解一下 preload、prefetch,从字面意思上讲:预加载,不难理解,就是提前加载资源(匹配其他页面可能用到的资源进行预先,从而达到无loading ,用户无感知的跳转),它的使用也非常的简单,在你要进行预加载的资源上添加 rel=“preload"标签即可;示例:<link rel=“preload” href=“index.css” as=“style” />而preload-webpack-plugin它的作用就是在编译打包的时候,帮我们把以上的操作都做了,编译完成后,你可以(指定某些/全部)文件动态插入到 HtmlWebPackPlugin 配置输出的文件内,as: 表示你预加载的资源类型;可以有有先多:script、font、image、style、video等等,更多详细请查看API,它还可以返回function;include:要插入,进行预加载的文件,它有:allChunks、initial、asyncChunks、数组等选项,数组即表示指定插入某些文件fileBlacklist:即文件黑名单,可以指定某个文件,也可以使用正则来匹配;//注意点1:请把配置一定写在HtmlWebPackPlugin插件之后,否则会报HtmlWebpackPlugin.getHooks is not a function错误,//注意点2:webpack4之后,请使用最新版本 npm install –save-dev preload-webpack-plugin@next,new PreloadWebpackPlugin({ rel: ‘prefetch’, as: ‘script’, // as(entry) { // if (/.css$/.test(entry)) return ‘style’; // return ‘script’;//首席填坑官∙苏南的专栏,QQ:912594095 // }, include: ‘asyncChunks’, // fileBlacklist: [“index.css”] fileBlacklist: [/\index.css|index.js|vendors.js/, /.whatever/]})webpack-bundle-analyzer这个插件还是蛮棒的,强烈推荐可以看看,也是本次分享的最后一个插件它的作用在于能帮我们很清晰直观的看到,你编译后的每一个、每一个文件哦,内容的分布组成,有利于我们快速查找包过大、内容是否重复、问题定位优化等;它会在编译完成后,自动启动一个服务、也可以自定义配置,打开一个可视化窗口,鼠标移动到对应的模块上,都可以显示出,该模块在某文件内占比的大小及stat、parsed、gzipped等的状态;analyzerHost、analyzerPort:自定配置打开的地址、端口,默认使用:127.0.0.1:8888reportFilename: 报告生成的路径,默认以项目的output.path输出;openAnalyzer:是否要自动打开分析窗口,其他还有很多属性,官网也有,这里只是引导简介,请大佬们勿喷;plugins:[ new BundleAnalyzerPlugin({…}) //默认配置就很好了,能满足我们的要求]常用的几个插件地址汇总:mini-css-extract-plugin 样式提取插件optimize-css-assets-webpack-plugin 样式优化压缩/配合添加前缀等html-webpack-plugin 生成入口文件,并注入依赖uglifyjs-webpack-plugin js压缩preload-webpack-plugin 资源预加载webpack-bundle-analyzer 可视化编译分析copy-webpack-plugin 文件拷贝BannerPlugin 给文件开头处添加注释typings-for-css-modules-loaderawesome-typescript-loader结尾:完整配置示例 以上就是今天为大家整理的几个项目中常用的插件,可能有些地方理解的不是特别到位,欢迎大家补充,同时我也给大家准备了一个整合后完整的webpack配置的示例,如有兴趣可自行测试。 下一期计划跟大家一起分享“React如何封装一个组件”(或者说沉淀一个组件库)来简单实战一下react如何上手?欢迎持续关注,如觉得不错记得点个赞哦,当然您能动动手指关注下方公众号就更棒了,谢谢支持!更多文章:如何给localStorage设置一个过期时间?如何用CSS3画出懂你的3D魔方?SVG Sprites Icon的使用技巧immutability因React官方出镜之使用总结分享!小程序项目之又一填坑小记做完小程序项目、老板给我加了6k薪资~面试踩过的坑,都在这里了~你应该做的前端性能优化之总结大全!如何给localStorage设置一个过期时间?手把手教你如何绘制一辆会跑车作者:苏南 - 首席填坑官链接:https://blog.csdn.net/weixin_…交流:912594095、公众号:honeyBadger8本文原创,著作权归作者所有。商业转载请联系@IT·平头哥联盟获得授权,非商业转载请注明原链接及出处。 ...

November 21, 2018 · 2 min · jiezi

如何给localStorage设置一个过期时间?

引言 这个话题其实在上次分享<小程序填坑记里讲过了>已经讲过(大佬可绕过哦~),但后来群里/评论都有些同学,提到了一些疑问,问能否单独整理一篇更为详细的分享,讲解一下细节和完善提到的不足,如是有了下文????。 —— 「 用心分享 做有温度的攻城狮,我是首席填坑官——苏南 」各位大佬早安,这里是@IT·平头哥联盟,我是首席填坑官∙苏南,用心分享 做有温度的攻城狮。公众号:honeyBadger8,群:912594095思考点 从我们接触前端起,第一个熟悉的存储相关的Cookie或者来分析我们生活中密切相关的淘宝、物流、闹钟等事物来说起吧,Cookie从你设置的时候,就会给个时间,不设置默认会话结束就过期;淘宝购物 从你下单付款起,就会给这件货物设置一个收货期限时间,过了这个时间自动认为你收货(即订单结束);闹钟 你设置的提醒时间,其实也就是它的过期时间;再比如与您每天切身相关的产品需求,过完需求,你给出的上线时间,也就是这个需求的过期时间;再通俗点讲,您今年的生日过完到明年生日之间也是相当于设置了有效期时间;以上种种,我们能得出一个结论任何一件事、一个行为动作,都有一个时间、一个节点,甚至我们可以黑localStorage,就是一个完善的API,为什么不能给一个设置过期的机制,因为sessionStorage、Cookie并不能满足我们实际的需求。实现思路 抱歉,黑localStorage不完善,有点夸张了,综合上述的总结,问题就简单了,给localStorage一个过期时间,一切就都so easy ?到底是不是,来看看具体的实现吧:简单回顾//示例一:localStorage.setItem(’test’,1234567);let test = localStorage.getItem(’test’);console.log(typeof test, test); //示例二:localStorage[’name’] = ‘苏南’;console.log(localStorage[’name’]);/输出:“1234567” ,‘苏南’,这里要注意,1234567 存进去时是number 取出来就成string了/重写 set(存入) 方法:首先有三个参数 key、value、expired ,分别对应 键、值、过期时间,过期时间的单位可以自由发挥,小时、分钟、天都可以,注意点:存储的值可能是数组/对象,不能直接存储,需要转换 JSON.stringify,这个时间如何设置呢?在这个值存入的时候在键(key)的基础上扩展一个字段,如:key+’expires’,而它的值为当前 时间戳 + expired过期时间具体来看一下代码 :set(key, value, expired) { /* * set 存储方法 * @ param {String} key 键 * @ param {String} value 值, * @ param {String} expired 过期时间,以分钟为单位,非必须 * @ 由@IT·平头哥联盟-首席填坑官∙苏南 分享,交流:912594095 / let source = this.source; source[key] = JSON.stringify(value); if (expired){ source[${key}__expires__] = Date.now() + 100060expired }; return value;}重写 get(获取) 方法:获取数据时,先判断之前存储的时间有效期,与当前的时间进行对比;但存储时expired为非必须参数,所以默认为当前时间+1,即长期有效;如果存储时有设置过期时间,且在获取的时候发现已经小于当前时间戳,则执行删除操作,并返回空值;注意点:存储的值可能是数组/对象,取出后不能直接返回,需要转换 JSON.parse,具体来看一下代码 :get(key) { / * get 获取方法 * @ param {String} key 键 * @ param {String} expired 存储时为非必须字段,所以有可能取不到,默认为 Date.now+1 * @ 由@IT·平头哥联盟-首席填坑官∙苏南 分享,交流:912594095 / const source = this.source, expired = source[${key}__expires__]||Date.now+1; const now = Date.now(); if ( now >= expired ) { this.remove(key); return; } const value = source[key] ? JSON.parse(source[key]) : source[key]; return value;}重写 remove(删除) 方法:删除操作就简单了,;remove(key) { const data = this.source, value = data[key]; //首席填坑官∙苏南的专栏 delete data[key]; delete data[${key}__expires__]; return value;}优化点:记得上次有个同学,是这么评论的:「 删除缓存能放到constructor里面执行么,放到get里面 不取就一直在那是不是不太好?」;所以本次优化做一个初始化删除操作,清除已经过期的数据;为什么不用for in而是 for ? for in循环遍历对象的属性时,原型链上的所有属性都将被访问,解决方案:使用hasOwnProperty方法过滤或Object.keys会返回自身可枚举属性组成的数组;class storage { constructor(props) { this.props = props || {} this.source = this.props.source || window.localStorage this.initRun(); } initRun(){ / * set 存储方法 * @ param {String} key 键 * @ param {String} value 值,存储的值可能是数组/对象,不能直接存储,需要转换 JSON.stringify * @ param {String} expired 过期时间,以分钟为单位 * @ 由@IT·平头哥联盟-首席填坑官∙苏南 分享,交流:912594095 */ const reg = new RegExp("expires"); let data = this.source; let list = Object.keys(data); if(list.length > 0){ list.map((key,v)=>{ if( !reg.test(key )){ let now = Date.now(); let expires = data[${key}__expires__]||Date.now+1; if (now >= expires ) { this.remove(key); }; }; return key; }); }; }}总结: 以上就是今天为大家总结的分享,您GET到了吗?小程序、sessionStorage、localStorage,都适用,做些许调整即可哦,希望今天的分享能给您带来些许成长,如果觉得不错,记得关注下方公众号哦,每周第一时间为您推最新分享????????。更多文章:easy-mock 最好的备胎没有之一immutability因React官方出镜之使用总结分享!面试踩过的坑,都在这里了~你应该做的前端性能优化之总结大全!如何给localStorage设置一个过期时间?动画一点点 - 如何用CSS3画出懂你的3D魔方?作者:苏南 - 首席填坑官链接:https://blog.csdn.net/weixin_…交流:关注公众号邀请您加入交流群 honeyBadger8本文原创,著作权归作者所有。商业转载请联系@IT·平头哥联盟获得授权,非商业转载请注明原链接及出处。 ...

November 15, 2018 · 2 min · jiezi

随我来基于webpack构建一个简易的vue脚手架 (webpack系列二)

前言之前有写了一篇webpack的文章(认识篇) 猛戳,大家对于webpack基本概念(entry,output,loader,plugin,mode…)应该是有了较模糊的认识.今天希望在通过(对于vue-cli的效仿)搭建一个自己的脚手架的途中对于概念会有更深刻的认识.目录1:搭建自己的项目模板(template) (基于vue的模板)2:生成对应的init命令,也就是脚手架构建命令以及上传NPM包 , 方便之后模板的使用 (分开俩篇来讲,方便你我 下一篇见)一: 模板构建先来个鸡汤 (这是个什么玩意啊怎么这么简单,我没问题分分钟掌握它) 摆正心态 那么follow me !!!初步构建mkdir my-vue-cli && cd my-vue-cli // 新建一个文件 并进入更目录 mkdir 是linux命令npm init -y // 初始一个packjage.json文件 -y 表示跳过询问步骤…完善项目结构//生成如下目录 ├── src //源目录(输入目录) │ ├── index.js │ ├── app.vue + |── index.html ├── package.json |── webpack.config.js //webpack配置文件 下载所需要的依赖(不太清楚的依次会介绍一下) npm install –save-dev vue 基于vue的那么vue必不可少…不多介绍 npm install –save-dev webpack 基于webpack的那么webpack也必不可少…不多介绍 npm install –save-dev webpack-cli webpack version 4+ 需要下载webpack-cli(一些指令下文可能涉及到) npm install –save-dev path path 模块提供了一些工具函数,用于处理文件与目录的路径。 npm install –save-dev html-webpack-plugin 简化了HTML文件的创建 Plugin that simplifies creation of HTML files to serve your bundles` npm install –save-dev clean-webpack-plugin 用于构建时清理构建文件夹下的内容 A webpack plugin to remove/clean your build folder(s) before building npm install –save-dev vue-loader Vue.js组件加载器(插件) npm install –save-dev vue-template-compiler 对于模板的函数编译 与vue-loader 配合使用 npm install –save-dev webpack-dev-server 热更新需要搭建服务模块项目代码构建src/index.jsimport Vue from ‘vue’ // 引入vue模块import App from ‘./app.vue’ //引入文件(组件) appnew Vue({ //vue写法 新建一个实例 el:"#app", //元素 template:’<App/>’, // 模板使用标签<app/> components:{App} // 组件app})src/app.vue<template> <div id=“app”> <p class=“test”>vue-cli-test vue-cli-test vue-cli-test </p> <p class=“test”>{{msg}}</p> </div></template><script>import Vue from ‘vue’ export default { name:‘app’, data(){ return { msg:“hello vue !!” } }, }</script><style > .test{ color:#020202; font-size:18px; }</style>webpack.config.jsconst path = require(‘path’); //path 模块提供了一些工具函数,用于处理文件与目录的路径。const HtmlWebpackPlugin = require(‘html-webpack-plugin’); //构建html文件const CleanWebpackPlugin = require(‘clean-webpack-plugin’); // 清理构建目录下的文件const webpack = require(‘webpack’); //webpack打包工具const VueLoaderPlugin = require(‘vue-loader/lib/plugin’); // vue-loader 编译vue文件const compiler = require(‘vue-template-compiler’) // 模板函数编译 与vue-loader配合使用module.exports = { entry: { //入口 “app”:"./src/index.js" }, module:{ //处理项目中的不同类型的模块。 rules:[ // rules 各种规则(数组类型) 每个规则可以分为三部分 - 条件(condition),结果(result)和嵌套规则(nested rule) { test:/.css/, use: [‘style-loader’, ‘css-loader’] // style-loader 和css-loader 编译css处理 }, { test: /.vue$/, loader: ‘vue-loader’ //vue-loader 编译vue模块 } ] }, devtool: ‘inline-source-map’, //生曾map 映射对应代码 方便错误查询 devServer:{ contentBase: ‘./dist’, // 告诉服务从哪提供代码内容(静态文件这么使用) hot:true //hot模式开启 }, plugins:[ new CleanWebpackPlugin([‘dist’]), // 告诉清理插件的目录 new HtmlWebpackPlugin({ // 构建html filename:‘index.html’, //文件名 title:‘my-vue-cli’, //title template:’./index.html’, //参照模板样式 }), new webpack.HotModuleReplacementPlugin(), //热模块替换开启 new VueLoaderPlugin() //vue-loader插件开启 ], output: { //出口 filename: ‘index.js’, //文件名 path: path.resolve(__dirname, ‘dist’), //路径 publicPath:"" //srcript 引入路径 }, resolve:{ //引入路径是不用写对应的后缀名 extensions: [’.js’, ‘.vue’, ‘.json’], alias:{ //正在使用的是vue的运行时版本,而此版本中的编译器时不可用的,我们需要把它切换成运行时 + 编译的版本 ‘vue$’:‘vue/dist/vue.esm.js’, //用@直接指引到src目录下 ‘@’: path.resolve(__dirname,’./src’), } }, };package.json添加script命令"scripts": { “test”: “echo "Error: no test specified" && exit 1”, “watch”: “webpack –watch”, “dev”: “webpack-dev-server –open –hot”, “build”: “webpack”},npm run dev 运行于8080/可看到预期效果.npm run build 打包编译同样可以看到效果 skr~~~~~~~~~github代码仓库,猛戳结尾本篇只是介绍基于vue,webpack的vue-cli简易搭建(因为init构建命令这些说好讲是好讲,但是讲太粗怕大家不太明白,那不如单独拿一篇出来让大家看),根据本文大家可以根据需求进行完善搞一个自己的脚手架.之后用自己的开发..是不是想想挺美~~ 快去行动吧.ps:想提前看看构建命令效果的同学 > git仓库,戳一戳把ps:(有我讲的不明白的地方,评论区见.我来完善) ...

November 12, 2018 · 2 min · jiezi

Webpack原理与实践(一):打包流程

写在前面的话在阅读 webpack4.x 源码的过程中,参考了《深入浅出webpack》一书和众多大神的文章,结合自己的一点体会,总结如下。总述webpack 就像一条生产线,要经过一系列处理流程后才能将源文件转换成输出结果。 这条生产线上的每个处理流程的职责都是单一的,多个流程之间有存在依赖关系,只有完成当前处理后才能交给下一个流程去处理。 插件就像是一个插入到生产线中的一个功能,在特定的时机对生产线上的资源做处理。 webpack 通过 Tapable 来组织这条复杂的生产线。 webpack 在运行过程中会广播事件,插件只需要监听它所关心的事件,就能加入到这条生产线中,去改变生产线的运作。 webpack 的事件流机制保证了插件的有序性,使得整个系统扩展性很好。 –吴浩麟《深入浅出webpack》核心的概念entry,loader,plugin,module,chunk 不论文档还是相关的介绍都很多了,不赘述,有疑问的移步文档。构建流程webpack 的运行流程是一个串行的过程,从启动到结束会依次执行以下流程:初始化参数:从配置文件和 Shell 语句中读取与合并参数,得出最终的参数;开始编译:用上一步得到的参数初始化 Compiler 对象,加载所有配置的插件,执行对象的 run 方法开始执行编译;确定入口:根据配置中的 entry 找出所有的入口文件编译模块:从入口文件出发,调用所有配置的 Loader 对模块进行翻译,再找出该模块依赖的模块,再递归本步骤直到所有入口依赖的文件都经过了本步骤的处理;完成模块编译:在经过第4步使用 Loader 翻译完所有模块后,得到了每个模块被翻译后的最终内容以及它们之间的依赖关系;输出资源:根据入口和模块之间的依赖关系,组装成一个个包含多个模块的 Chunk,再把每个 Chunk 转换成一个单独的文件加入到输出列表,这步是可以修改输出内容的最后机会;输出完成:在确定好输出内容后,根据配置确定输出的路径和文件名,把文件内容写入到文件系统。在以上过程中,webpack 会在特定的时间点广播出特定的事件,插件在监听到感兴趣的事件后会执行特定的逻辑,并且插件可以调用 webpack 提供的 API 改变 webpack 的运行结果。webpack 中比较核心的两个对象Compile 对象:负责文件监听和启动编译。Compiler 实例中包含了完整的 webpack 配置,全局只有一个 Compiler 实例。compilation 对象:当 webpack 以开发模式运行时,每当检测到文件变化,一次新的 Compilation 将被创建。一个 Compilation 对象包含了当前的模块资源、编译生成资源、变化的文件等。Compilation 对象也提供了很多事件回调供插件做扩展。这两个对象都继承自 Tapable。以Compile为例const { Tapable, SyncHook, SyncBailHook, AsyncParallelHook, AsyncSeriesHook} = require(“tapable”);class Compiler extends Tapable { constructor(context) { super(); this.hooks = { /** @type {SyncBailHook<Compilation>} / //所有需要输出的文件已经生成好,询问插件哪些文件需要输出,哪些不需要。 shouldEmit: new SyncBailHook([“compilation”]), /* @type {AsyncSeriesHook<Stats>} / //成功完成一次完成的编译和输出流程。 done: new AsyncSeriesHook([“stats”]), /* @type {AsyncSeriesHook<>} / additionalPass: new AsyncSeriesHook([]), /* @type {AsyncSeriesHook<Compiler>} / beforeRun: new AsyncSeriesHook([“compiler”]), /* @type {AsyncSeriesHook<Compiler>} / //启动一次新的编译 run: new AsyncSeriesHook([“compiler”]), /* @type {AsyncSeriesHook<Compilation>} / // 确定好要输出哪些文件后,执行文件输出,可以在这里获取和修改输出内容。 emit: new AsyncSeriesHook([“compilation”]), /* @type {AsyncSeriesHook<Compilation>} / // 输出完毕 afterEmit: new AsyncSeriesHook([“compilation”]), // 以上几个事件(除了run,beforerun为编译阶段)其余为输出阶段的事件 /* @type {SyncHook<Compilation, CompilationParams>} / // compilation 创建之前挂载插件的过程 thisCompilation: new SyncHook([“compilation”, “params”]), /* @type {SyncHook<Compilation, CompilationParams>} / // 创建compilation对象 compilation: new SyncHook([“compilation”, “params”]), /* @type {SyncHook<NormalModuleFactory>} / // 初始化阶段:初始化compilation参数 normalModuleFactory: new SyncHook([“normalModuleFactory”]), /* @type {SyncHook<ContextModuleFactory>} / // 初始化阶段:初始化compilation参数 contextModuleFactory: new SyncHook([“contextModulefactory”]), /* @type {AsyncSeriesHook<CompilationParams>} / beforeCompile: new AsyncSeriesHook([“params”]), /* @type {SyncHook<CompilationParams>} / // 该事件是为了告诉插件一次新的编译将要启动,同时会给插件带上 compiler 对象 compile: new SyncHook([“params”]), /* @type {AsyncParallelHook<Compilation>} / //一个新的 Compilation 创建完毕,即将从 Entry 开始读取文件,根据文件类型和配置的 Loader 对文件进行编译,编译完后再找出该文件依赖的文件,递归的编译和解析。 make: new AsyncParallelHook([“compilation”]), /* @type {AsyncSeriesHook<Compilation>} / // 一次Compilation执行完成 afterCompile: new AsyncSeriesHook([“compilation”]), /* @type {AsyncSeriesHook<Compiler>} / //监听模式下启动编译(常用于开发阶段) watchRun: new AsyncSeriesHook([“compiler”]), /* @type {SyncHook<Error>} / failed: new SyncHook([“error”]), /* @type {SyncHook<string, string>} / invalid: new SyncHook([“filename”, “changeTime”]), /* @type {SyncHook} / // 如名字所述 watchClose: new SyncHook([]), // TODO the following hooks are weirdly located here // TODO move them for webpack 5 /* @type {SyncHook} / //初始化阶段:开始应用 Node.js 风格的文件系统到compiler 对象,以方便后续的文件寻找和读取。 environment: new SyncHook([]), /* @type {SyncHook} / // 参照上文 afterEnvironment: new SyncHook([]), /* @type {SyncHook<Compiler>} / // 调用完内置插件以及配置引入插件的apply方法,完成了事件订阅 afterPlugins: new SyncHook([“compiler”]), /* @type {SyncHook<Compiler>} / afterResolvers: new SyncHook([“compiler”]), /* @type {SyncBailHook<string, EntryOptions>} */ // 读取配置的 Entrys,为每个 Entry 实例化一个对应的 EntryPlugin,为后面该 Entry 的递归解析工作做准备。 entryOption: new SyncBailHook([“context”, “entry”]) };在 webpack 执行的过程中,会按顺序广播一系列事件–this.hooks中的一系列事件(类似于我们常用框架中的生命周期),而这些事件的订阅者该按照怎样的顺序来组织,来执行,来进行参数传递… 这就是 Tapable 要做的事情。 关于 Tapable 给大家推荐一篇比较好(但是阅读量点赞评论都不多2333)的科普文流程细节流程细节参照我在引用的Compile对象中的注释,有一点需要注意,作者hooks的书写顺序并不是调用顺序。有些没注释的有几种情况:不那么重要,或参照事件名称和上下文可知主要是暂时还不知道(2333,后面有新的理解再补充,逃…)当然最重要的事件基本涵盖到了这里补充一个大从参考文章里面找来的图compilation 过程简介compilation 实际上就是调用相应的 loader 处理文件生成 chunks并对这些 chunks 做优化的过程。几个关键的事件(Compilation对象this.hooks中):buildModule 使用对应的 Loader 去转换一个模块;normalModuleLoader 在用 Loader 对一个模块转换完后,使用 acorn 解析转换后的内容,输出对应的抽象语法树(AST),以方便 webpack 后面对代码的分析。seal 所有模块及其依赖的模块都通过 Loader 转换完成后,根据依赖关系开始生成 Chunk。最后从参考文章中摘了一张图片以便于对整个过程有更清晰的认知参考http://taobaofed.org/blog/201…http://imweb.io/topic/5baca58…《深入浅出webpack》广而告之本文发布于薄荷前端周刊,欢迎Watch & Star ★,转载请注明出处。欢迎讨论,点个赞再走吧 。◕‿◕。 ~ ...

November 12, 2018 · 2 min · jiezi

webpack4配置详解之逐行分析

前言 经常会有群友问起webpack、react、redux、甚至create-react-app配置等等方面的问题,有些是我也不懂的,慢慢从大家的相互交流中,也学到了不少。 今天就尝试着一起来聊聊Webpack吧,旨在帮大家加深理解、新手更容易上路,都能从0到1搭建配置自定属于自己的脚手架,或对已封装好的脚手架有进一步的巩固,接下来苏南会详细讲解webpack中的每一个配置字段的作用(部分为webpack4新增)。近两年,前端一直在以一个高速持续的过程发展,常常会有网友在调侃老了、学不动了,虽是在调侃却又间接阐述着无奈,迫于生活的压力,不得不提速前行,因为没有谁为你而停留,公司不会、社会不会、同伴不会……,停下可能将意味着淘汰 —— 理想很丰满,现实很骨感,所以让我们一起进步,共同加薪,奋斗吧骚年,加油。。~~吐槽过了,接着聊正事~~。 原谅我控制不住自己,想问下各位,昨天刚刚过去的双十一你脱单了吗?人生若只如初见,何事秋风悲画扇;等闲变却故人心,却道故人心易变;骊山语罢清宵半,夜雨霖铃终不怨。各位大佬早安,这里是@IT·平头哥联盟,我是首席填坑官∙苏南,用心分享 做有温度的攻城狮。公众号:honeyBadger8,群:912594095entry这个不用解释了,看名字就是知道,它就是通往天堂/地狱的入口,一切的苦难从这里开始,也从这里结束。简单介绍几种写法://方式一:单文件写法entry: { index: ‘./src/pages/route.js’, //about: ‘./src/pages/about.js’, //other:()=>{…} //首席填坑官∙苏南的专栏,公众号:honeyBadger8}//方式二:多文件写法entry: { /index:[ //首席填坑官∙苏南的专栏 ‘webpack-hot-middleware/client’, ‘./src/root.js’ ],/ index: [’./src/root.js’], vendors : [‘react’,‘react-dom’,‘redux’,‘react-router’,‘classnames’],}output - 输出它位于对象最顶级键(非常重要),如果说entry是一扇门,output就是审判官,决定着你是上天堂还是入地狱;指示 webpack 如何去输出、以及在哪里输出、输出的格式等;path: 输出文件的目录,filename:输出的文件名,它一般跟你entry配置相对应,如:js/[name].js name在这里表示的是[index、vendors],chunkFilename:块,配置了它,非入口entry的模块,会帮自动拆分文件,也就是大家常说的按需加载,与路由中的 require.ensure相互应publicPath:文件输出的公共路径,pathinfo:即保留相互依赖的包中的注释信息,这个基本不用主动设置它,它默认 development 模式时的默认值是 true,而在 production 模式时的默认值是 false,主要的就是这些,还有一些其他的library、libraryTarget、auxiliaryComment等,感兴趣的可自行了解,output: { path: path.resolve(_dirname, ‘../assets’), filename: ‘js/[name].js’, chunkFilename: ‘js/[name].[chunkhash:8].js’, publicPath: ‘/static/’, //最终访问的路径就是:localhost:3000/static/js/.js //pathinfo:true,}hash常用的有三种:模板描述hash模块标识符的hash,一般应用于filename:’[name].[hash].js’chunkhash按需分块内容的 hash,它是根据chunk自身的内容计算而来contenthash这个没有用过,看了下文档它是在提取css文件时根据内容计算而来的 hash ,结合ExtractTextWebpackPlugin插件使用hash长度默认20,可自定:[hash:8]、[chunkhash:16]mode这个属于webpack4才新增的,4之前大家一般用DefinePlugin插件设置mode:development,production,none,development : 开发模式,打包的代码不会被压缩,开启代码调试,production : 生产模式,则正好反之。//方法一webpack –mode development/production//方法二……mode:‘development/production’……devtool控制是否生成,以及如何生成 source map文件,开发环境下更有利于定位问题,默认 false,当然它的开启,也会影响编译的速度,所以生产环境一定一定记得关闭;常用的值:cheap-eval-source-map、eval-source-map、cheap-module-eval-source-map、inline-cheap-module-source-map等等,更详细的可以去官方查看;本人一般使用:eval-source-map较多,每个都有它不一样的特性,有兴趣的同学可以一一尝试,optimizationoptimization是webpack4新增的,主要是用来让开发者根据需要自定义一些优化构建打包的策略配置,minimize:true/false,告诉webpack是否开启代码最小化压缩,minimizer:自定js优化配置,会覆盖默认的配置,结合UglifyJsPlugin插件使用,removeEmptyChunks: bool 值,它检测并删除空的块。将设置为false将禁用此优化,removeEmptyChunks: bool 值,它检测并删除空的块。将设置为false将禁用此优化,nodeEnv:它并不是node里的环境变量,设置后可以在代码里使用 process.env.NODE_ENV === ‘development’来判断一些逻辑,生产环境UglifyJsPlugin会自动删除无用代码,splitChunks :取代了CommonsChunkPlugin,自动分包拆分、代码拆分,详细默认配置:默认配置,只会作用于异步加载的代码块 —— chunks: ‘async’,它有三个值:all,async,initial//环境变更也可以直接 在启动中设置 //webpack –env.NODE_ENV=local –env.production –progress//splitChunks 默认配置 ,首席填坑官∙苏南的专栏splitChunks: { chunks: ‘async’, minSize: 30000, maxSize: 0, minChunks: 1, maxAsyncRequests: 5, maxInitialRequests: 3, automaticNameDelimiter: ‘~’, name: true, cacheGroups: { vendors: { test: /[\/]node_modules[\/]/, priority: -10 }, default: { minChunks: 2, priority: -20, reuseExistingChunk: true } }}runtimeChunk: 提取 webpack 运行时代码,它可以设置为:boolean、Object该配置开启时,会覆盖 入口指定的名称!!!optimization: { runtimeChunk:true,//方式一 runtimeChunk: { name: entrypoint => runtimechunk~${entrypoint.name} //方式二 }}resolve - 配置模块如何解析extensions:自动解析确定的扩展,省去你引入组件时写后缀的麻烦,alias:非常重要的一个配置,它可以配置一些短路径,modules:webpack 解析模块时应该搜索的目录,其他 plugins、unsafeCache、enforceExtension,基本没有怎么用到,//extensions 后缀可以省略,import Toast from ‘src/components/toast’; // alias ,短路径import Modal from ‘../../../components/modal’ //简写 ,首席填坑官∙苏南的专栏import Modal from ‘src/components/modal’ resolve: { extensions: [’.js’, ‘.jsx’,’.ts’,’.tsx’, ‘.scss’,’.json’,’.css’], alias: { src :path.resolve(__dirname, ‘../src’), components :path.resolve(__dirname, ‘../src/components’), utils :path.resolve(__dirname, ‘../src/utils’), }, modules: [’node_modules’],},module.rules - 编译规则,rules:也就是之前的loaders,test : 正则表达式,匹配编译的文件,exclude:排除特定条件,如通常会写node_modules,即把某些目录/文件过滤掉,include:它正好与exclude相反,use -loader :必须要有它,它相当于是一个 test 匹配到的文件对应的解析器,babel-loader、style-loader、sass-loader、url-loader等等,use - options:它与loader配合使用,可以是一个字符串或对象,它的配置可以直接简写在loader内一起,它下面还有presets、plugins等属性;具体来看一下示例:module: { rules: [ { test: /.(js|jsx)$/, exclude: /node_modules/, use: [ { loader: ‘babel-loader’, options: { presets: [ [’env’, { targets: { browsers: CSS_BROWSERS, }, }],‘react’, ’es2015’, ‘stage-0’ ], plugins: [ ’transform-runtime’, ‘add-module-exports’, ], }, }, ], }, { test: /.(scss|css)$/, use: [ ‘style-loader’, {loader: ‘css-loader’,options:{plugins: [require(‘autoprefixer’)({browsers: CSS_BROWSERS,}),],sourceMap: true}}, {loader: ‘postcss-loader’,options:{plugins: [require(‘autoprefixer’)({browsers: CSS_BROWSERS,}),],sourceMap: true}}, {loader: ‘sass-loader’,options:{sourceMap: true}} ] }, { test: /.(png|jpg|jpeg|gif)$/, exclude: /node_modules/, use: [ { loader: ‘url-loader?limit=12&name=images/[name].[hash:8].[ext]’, }, ], }, { test: /.(woff|woff2|ttf|eot|svg)$/, exclude: /node_modules/,//首席填坑官∙苏南的专栏,公众号:honeyBadger8 use: [ { loader: ‘file-loader?name=fonts/[name].[hash:8].[ext]’, }, ], }, ],},项目中常用loaderbabel-loader、awesome-typescript-loader js/ts编译,css-loader、postcss-loader、sass-loader、less-loader、style-loader 等css样式处理file-loader、url-loader、html-loader等图片/svg/html等的处理,plugins - 插件UglifyJsPluginHotModuleReplacementPluginNoEmitOnErrorsPluginHtmlWebPackPluginExtractTextPluginPreloadWebpackPlugin等等,很多很多,插件的详解会留在下一章节详细介绍,欢迎持续关注。plugins/loader 区别新入门的一些同学可能会有些疑惑,不是有loader了吗?为什么还plugins呢,还要它做什么?loader的作用在于解析文件,比如把ES6转换成es5,甚至ES3,毕竟还有万恶的IE嘛;把Sass、Less解析成CSS,给CSS自动加上兼容的前缀;对图片进行一个解析等等;plugins呢?它在干啥?它在吹水、喝茶、嗑瓜子聊天,当然这是loader在没有把项目做完之前,loader下班时间就是plugins苦难的开始,它要对loader干的事情进行优化分类、提取精华(公共代码提取)、做压缩处理(js/css/html压缩)、输出指定的目录等……,反正也是很苦逼!webpack-dev-server这个有些老生常谈了,新手上路一般都有用它,公司因为现在是结合了 微服务,整套流程是结合:Dockerfile、nodejs、express等一起在线构建编译的,所以大部分项目都不会走webpack-dev-server;我们开发环境就是使用 express + webpack-dev-middleware + webpack-hot-middleware+ ‘…’;contentBase :告诉服务(dev server)在哪里查找文件,默认不指定会在是当期项目根目录,historyApiFallback:可以是boolean、 object,默认响应的入口文件,包括404都会指向这里,object见下面示例:compress:启用 gzip 压缩,publicPath:它其实就是 output.publicPath,当你改变了它,即会覆盖了output的配置,stats: 可以自定控制要显示的编译细节信息,proxy:它其实就是http-proxy-middleware,可以进行处理一些代理的请求。//方式一:不配置方式二的内容 webpack-dev-server –config webpack/webpack.config.dev.js//指定 端口: –port=8080 //开启热更新:–hot//gzip: –compress//方式二devServer : contentBase:’./assets’, host: ‘0.0.0.0’, port: 9089, publicPath: ‘/assets/’, historyApiFallback: { index: ‘/views/index.html’ }, /* 匹配路径,进入不同的入口文件,首席填坑官∙苏南的专栏,公众号:honeyBadger8 rewrites: [ { from: /^/$/, to: ‘/views/landing.html’ }, { from: /^/subpage/, to: ‘/views/subpage.html’ }, { from: /./, to: ‘/views/404.html’ } ] } */ compress: true, noInfo: true, inline: true, hot: true, stats: { colors: true, chunks: false }, proxy:{ ‘/mockApi’: ‘https://easy-mock.com/project/5a0aad39eace86040263d' ,//请求可直接写成 /mockApi/api/login… }}webpack4删除的点:module.loadersNoErrorsPluginCommonsChunkPluginDefinePluginOccurenceOrderPlugin欢迎补充……,平时用到的大概就是这些尾声: 以上就是工作中react自定脚手架的配置总结,希望能对您有所帮助,webpack4的改动蛮大的,功能比之前强大了少,也简便了开发者很多的麻烦,效率大大提高,但同时也意味着我们对于底层的东西,了解的更少了,下一章节将为大家分享一些常用的插件/以及用法的分析,欢迎持续关注,记得点个赞哦,当然您能动动手指关注下方公众号就更棒了,谢谢支持!更多文章:easy-mock 最好的备胎没有之一immutability因React官方出镜之使用总结分享!小程序项目之做完项目老板给我加了6k薪资~小程序项目之填坑小记面试踩过的坑,都在这里了~你应该做的前端性能优化之总结大全!如何给localStorage设置一个过期时间?动画一点点 - 如何用CSS3画出懂你的3D魔方?动画一点点 - 手把手教你如何绘制一辆会跑车SVG Sprites Icon的使用技巧作者:苏南 - 首席填坑官链接:https://blog.csdn.net/weixin…交流:912594095、公众号:honeyBadger8本文原创,著作权归作者所有。商业转载请联系@IT·平头哥联盟获得授权,非商业转载请注明原链接及出处。 ...

November 12, 2018 · 2 min · jiezi

还不打算去认识一下webpack?

前言随我来,去看看webpack!(为时未晚)============》第一版(较浅显的知识,懂得可忽略本文)方向安装,起步搭建运行. (粗略代过)对于资源的管理,对于输出的管理. (举例介绍)本地开发 (基础服务)热更新=[模块热替换] (初步认识)1.初步构建mkdir webpack_demo && cd webpack_demo // 新建一个文件 并进入更目录 mkdir 是linux命令npm init -y // 初始一个packjage.json文件 -y 表示跳过询问步骤…//安装webpacknpm install webpack –save-dev // 添加webpack-cli依赖到"devDependencies"//webpack4.0+ 需要安装webpack-cli npm install webpack-cli –save-dev // 添加webpack-cli依赖到"devDependencies" //生成如下目录├── package.json ├── src //源目录(输入目录)│ ├── index.js ├── dist // 输出目录│ ├── index.html // 修改 dist/index.html< !DOCTYPE html><html lang=“en”> <head> <meta charset=“UTF-8”> <title>webpack_demo</title> </head> <body> <script src=“main.js”></script> //为什么是main.js下面会解释 </body></html>//修改src/index.js function component() { var element = document.createElement(‘div’); element.innerHTML = “整一个盒子” return element; } document.body.appendChild(component()); npx webpack (Node 8.2+ 版本提供的 npx 命令) node node_modules/.bin/webpack (8.2-版本)会将我们的脚本作为入口起点,然后 输出 为 main.js.打开dist/index.html 你将会看到 整一个盒子 几个字样~2.资源管理,输出管理.基本开发起步//生成如下目录 ├── package.json + |── webpack.config.js //webpack配置文件 ├── src //源目录(输入目录) │ ├── index.js ├── dist // 输出目录 │ ├── index.html 先介绍一个Lodash库 它是一个一致性、模块化、高性能的 JavaScript 实用工具库 模块化处理非常适合值操作和检测(说白了就是webpack用了我也试试…) lodash相关文档npm install lodash –save //非仅在开发的时候使用的依赖 就是需要打包到生产环境的包 不加-dev// src/index.jsimport _ from ’lodash’;function component() { var element = document.createElement(‘div’); element.innerHTML = _.join([’lodash’,‘webpack’],’’); //join将 array 中的所有元素转换为由’‘分隔的字符串 其它函数可以自己实践 return element; }打开index页面输出 loadshwebpack//webpack.config.js const path = require(‘path’); module.exports = { entry: ‘./src/index.js’, //入口 output: { //出口 filename: ‘main.js’, //打包之后脚本文件名称 path: path.resolve(__dirname, ‘dist’) //路径指向执行 js 文件的绝对路径 此处为/dist } };执行npx webpack –config webpack.config.js (把之前dist目录下main.js删除) 新的脚本生成(其实没多大变化..)// 配置一下package.json “scripts”: { “test”: “echo "Error: no test specified" && exit 1”, “build”: “webpack” //添加此行命令 下次执行打包就是 npm run build 相当于上面的npx webpack –config webpack.config.js },// 资源的配置 css 图片 js等等.. 举例 css 图片npm install –save-dev style-loader css-loader css的loadernpm install –save-dev file-loader file(图片)对象的 loader //生成如下目录 ├── package.json + |── webpack.config.js //webpack配置文件 ├── src //源目录(输入目录) │ ├── index.js + │ ├── index.css + │ ├── icon.jpg ├── dist // 输出目录 │ ├── index.html //修改webpack.config.js const path = require(‘path’); //path路径模块 module.exports = { entry: ‘./src/index.js’, //入口 output: { //出口 filename: ‘main.js’, path: path.resolve(__dirname, ‘dist’) }, module: { rules: [ { test: /.css$/, //检测正则匹配.css结尾的文件 use: [ //使用俩个loader ‘style-loader’, ‘css-loader’ ] }, { test: /.(png|svg|jpg|gif)$/, //正则匹配.png svg jpg gif结尾的文件 use: [ //使用file-loader ‘file-loader’ ] } ] } }; //修改src/index.css div{ color:red; } //修改src/index.js import _ from ’lodash’; import “./index.css”; import Icon from ‘./icon.jpg’; function component() { var element = document.createElement(‘div’); element.innerHTML = _.join([’loadsh’, ‘webpack’], ’ ‘); var myIcon = new Image(); myIcon.src = Icon; element.appendChild(myIcon); return element; } document.body.appendChild(component());npm run build(删除之前的dist目录下main.js) 你会看红字和图片 以上就是资源管理的简短介绍npm install –save-dev html-webpack-plugin 安装html-webpack-plugin模块 模块用到功能: 1: 动态添加每次compile后 js css 的hash 2: 可配置多页面 单页面 这些 3: 其它没涉及到npm install clean-webpack-plugin –save-dev 清除dist文件夹(每次删除麻烦了..)配置一下//修改目录 ├── package.json |── webpack.config.js //webpack配置文件 ├── src //源目录(输入目录) + │ ├── app.js + │ ├── print.js │ ├── index.css │ ├── icon.jpg ├── dist // 输出目录 │ ├── index.html //webpack.config.js ===============================================const 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’ }, output: { filename: ‘[name].bundle.js’, path: path.resolve(__dirname, ‘dist’) }, plugins: [ new CleanWebpackPlugin([‘dist’]), new HtmlWebpackPlugin({ title: ‘webpack_demo’ }) ], module: { rules: [ { test: /.css$/, use: [ ‘style-loader’, ‘css-loader’ ] }, { test: /.(png|svg|jpg|gif)$/, use: [ ‘file-loader’ ] } ] }};//修改src/index.js =================================================== import _ from ’lodash’; //引入lodash模块 import “./index.css”; // index.css import Icon from ‘./icon.jpg’; // 图片 import printMe from “./print.js” // printJS function component() { var element = document.createElement(‘div’); //创建一个元素 element.innerHTML = .join([’loadsh’, ‘webpack’], ’ ‘); // lodash中.join方法 var myIcon = new Image(); //创建一个图片 myIcon.src = Icon; //src赋值 element.appendChild(myIcon); //追加图片 var btn = document.createElement(‘button’); //创建按钮 btn.innerHTML = ‘Click me and check the console!’; //内容赋值 btn.onclick = printMe; //添加事件 element.appendChild(btn); //追加元素 return element; } document.body.appendChild(component()); //追加元素到body中 //修改src/print.js ========================================== export default function printMe() { console.log(‘from print.js’); } npm run build 会发现基本webpack的配置之后 ,有点模样(意思)了 打开页面index.html正常访问3.本地开发npm install –save-dev webpack-dev-server “webpack-dev-server” 为你提供了一个简单的 web 服务器,并且能够实时重新加载 //修改webpack.config.jsconst path = require(‘path’);const HtmlWebpackPlugin = require(‘html-webpack-plugin’);const CleanWebpackPlugin = require(‘clean-webpack-plugin’);const webpack = require(‘webpack’);module.exports = { entry: { app: ‘./src/index.js’ }, output: { filename: ‘[name].bundle.js’, path: path.resolve(__dirname, ‘dist’) }, devServer: { contentBase: ‘./dist’ }, plugins: [ new CleanWebpackPlugin([‘dist’]), new HtmlWebpackPlugin({ title: ‘webpack_demo’ }) ], module: { rules: [ { test: /.css$/, use: [ ‘style-loader’, ‘css-loader’ ] }, { test: /.(png|svg|jpg|gif)$/, use: [ ‘file-loader’ ] } ] }};//修改package.json…“scripts”: { “test”: “echo "Error: no test specified" && exit 1”, “start”: “webpack-dev-server –open”, //start命令 “build”: “webpack” }, … npm run start 本地起了8080端口的服务,你也可以看到自己的页面4.热更新//修改webpack.config.jsconst path = require(‘path’);const HtmlWebpackPlugin = require(‘html-webpack-plugin’);const CleanWebpackPlugin = require(‘clean-webpack-plugin’);const webpack = require(‘webpack’);module.exports = { entry: { app: ‘./src/index.js’ }, output: { filename: ‘[name].bundle.js’, path: path.resolve(__dirname, ‘dist’) }, devServer: { contentBase: ‘./dist’, hot: true }, plugins: [ new CleanWebpackPlugin([‘dist’]), new HtmlWebpackPlugin({ title: ‘webpack_demo’ }), new webpack.NamedModulesPlugin(), new webpack.HotModuleReplacementPlugin() ], module: { rules: [ { test: /.css$/, use: [ ‘style-loader’, ‘css-loader’ ] }, { test: /.(png|svg|jpg|gif)$/, use: [ ‘file-loader’ ] } ] }}; npm run start 运行http://localhost:8080/ 然后你去修改print js的console(或者添加其他代码) 会发现命令行输出updated. Recompiling… 字样 这就是简单的实现了热更新最后本文只是大概从几个demo来对于webpack的基础概念 入口entry 出口 output loader plugins mode(没有直面涉及)几大模块的梳理于实践,让大家对于webpack不在那么陌生!后续文章会从更深入的角度去学习webpack! 暂定下周1 发表文章(内容 详细介绍hot 实现一个简易的vue-cli等等)demo的代码我会同步github ...

November 6, 2018 · 4 min · jiezi

webpack4 系列教程(十五):开发模式与webpack-dev-server

作者按:因为教程所示图片使用的是 github 仓库图片,网速过慢的朋友请移步《webpack4 系列教程(十五):开发模式与 webpack-dev-server》原文地址。更欢迎来我的小站看更多原创内容:godbmw.com,进行“姿势”交流 ♪(^∇^)0. 课程介绍和资料>>>本节课源码>>>所有课程源码本节课的代码目录如下:本节课用的 plugin 和 loader 的配置文件package.json如下:{ “scripts”: { “dev”: “webpack-dev-server –open” }, “devDependencies”: { “clean-webpack-plugin”: “^0.1.19”, “html-webpack-plugin”: “^3.2.0”, “jquery”: “^3.3.1”, “webpack”: “^4.16.1”, “webpack-cli”: “^3.1.0”, “webpack-dev-server”: “^3.1.4” }}1. 为什么需要开发模式?在之前的课程中,我们都没有指定参数mode。但是执行webpack进行打包的时候,自动设置为production,但是控制台会爆出warning的提示。而开发模式就是指定mode为development。在开发模式下,我们需要对代码进行调试。对应的配置就是:devtool设置为source-map。在非开发模式下,需要关闭此选项,以减小打包体积。在开发模式下,还需要热重载、路由重定向、挂代理等功能,webpack4已经提供了devServer选项,启动一个本地服务器,让开发者使用这些功能。2. 如何使用开发模式?根据文章开头的package.json的配置,只需要在命令行输入:npm run dev即可启动开发者模式。启动效果如下图所示:虽然控制台输出了打包信息(假设我们已经配置了热重载),但是磁盘上并没有创建/dist/文件夹和打包文件。控制台的打包文件的相关内容是存储在内存之中的。3. 编写一些需要的文件首先,编写一下入口的 html 文件:<!– index.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>Document</title></head><body> This is Index html</body></html>然后,按照项目目录,简单封装下/vendor/下的三个 js 文件,以方便app.js调用:// minus.jsmodule.exports = function(a, b) { return a - b;};// multi.jsdefine(function(require, factory) { “use strict”; return function(a, b) { return a * b; };});// sum.jsexport default function(a, b) { console.log(“I am sum.js”); return a + b;}好了,准备进入正题。4. 编写 webpack 配置文件4.1 配置代码由于配置内容有点多,所以放代码,再放讲解。webpack.config.js配置如下所示:const webpack = require(“webpack”);const HtmlWebpackPlugin = require(“html-webpack-plugin”);const path = require(“path”);module.exports = { entry: { app: “./app.js” }, output: { publicPath: “/”, path: path.resolve(__dirname, “dist”), filename: “[name]-[hash:5].bundle.js”, chunkFilename: “[name]-[hash:5].chunk.js” }, mode: “development”, // 开发模式 devtool: “source-map”, // 开启调试 devServer: { contentBase: path.join(__dirname, “dist”), port: 8000, // 本地服务器端口号 hot: true, // 热重载 overlay: true, // 如果代码出错,会在浏览器页面弹出“浮动层”。类似于 vue-cli 等脚手架 proxy: { // 跨域代理转发 “/comments”: { target: “https://m.weibo.cn”, changeOrigin: true, logLevel: “debug”, headers: { Cookie: "" } } }, historyApiFallback: { // HTML5 history模式 rewrites: [{ from: /./, to: “/index.html” }] } }, plugins: [ new HtmlWebpackPlugin({ filename: “index.html”, template: “./index.html”, chunks: [“app”] }), new webpack.HotModuleReplacementPlugin(), new webpack.NamedModulesPlugin(), new webpack.ProvidePlugin({ $: “jquery” }) ]};4.2 模块热更新模块热更新需要HotModuleReplacementPlugin和NamedModulesPlugin这两个插件,并且顺序不能错。并且指定devServer.hot为true。有了这两个插件,在项目的 js 代码中可以针对侦测到变更的文件并且做出相关处理。比如,我们启动开发模式后,修改了vendor/sum.js这个文件,此时,需要在浏览器的控制台打印一些信息。那么,app.js中就可以这么写:if (module.hot) { // 检测是否有模块热更新 module.hot.accept("./vendor/sum.js", function() { // 针对被更新的模块, 进行进一步操作 console.log("/vendor/sum.js is changed"); });}每当sum.js被修改后,都可以自动执行回调函数。4.3 跨域代理随着前后端分离开发的普及,跨域请求变得越来越常见。为了快速开发,可以利用devServer.proxy做一个代理转发,来绕过浏览器的跨域限制。按照前面的配置文件,如果想调用微博的一个接口:https://m.weibo.cn/comments/hotflow。只需要在代码中对/comments/hotflow进行请求即可:$.get( “/comments/hotflow”, { id: “4263554020904293”, mid: “4263554020904293”, max_id_type: “0” }, function(data) { console.log(data); });4.4 HTML5–History当项目使用HTML5 History API 时,任意的 404 响应都可能需要被替代为 index.html。在 SPA(单页应用)中,任何响应直接被替代为index.html。在 vuejs 官方的脚手架vue-cli中,开发模式下配置如下:// …historyApiFallback: { rewrites: [{ from: /.*/, to: “/index.html” }];}// …5. 编写入口文件最后,在前面所有的基础上,让我们来编写下入口文件app.js:import sum from “./vendor/sum”;console.log(“sum(1, 2) = “, sum(1, 2));var minus = require(”./vendor/minus”);console.log(“minus(1, 2) = “, minus(1, 2));require([”./vendor/multi”], function(multi) { console.log(“multi(1, 2) = “, multi(1, 2));});$.get( “/comments/hotflow”, { id: “4263554020904293”, mid: “4263554020904293”, max_id_type: “0” }, function(data) { console.log(data); });if (module.hot) { // 检测是否有模块热更新 module.hot.accept(”./vendor/sum.js”, function() { // 针对被更新的模块, 进行进一步操作 console.log("/vendor/sum.js is changed"); });}6. 效果检测在命令行键入:npm run dev开启服务器后,会自动打开浏览器。如下图所示:打开控制台,可以看到代码都正常运行没有出错。除此之外,由于开启了source-map,所以可以定位代码位置(下图绿框内):7. 参考资料dev-server 文档: https://www.webpackjs.com/configuration/dev-server/开发模式 文档:https://www.webpackjs.com/guides/development/ ...

October 22, 2018 · 2 min · jiezi

webpack4带来了什么

在开发人员还在体会webpack3.x的余韵时,webpack4.x已经悄然而来。而对使用者来说,最期待的问题无外乎如下:新版本与旧版本相比都有哪些改变?webpack3.x到webapck4.x的迁移?使用webpack4.x我们应该注意什么?webpack的新特性webpack 作为构建工具的强大之处在于:可以在 webpack.config.js 中配置很多独特的功能;它的配置灵活多变;但正因为这样,这也是它的糟点。因为太随意,所以不好控制,造成了如下的问题:学习、使用、研究webpack的成本过高(进阶曲线太陡);构建一个小应用也需要像构建大应用那样配置 webpack.config.js(麻雀虽小五脏俱全);而webpack4.x作为新一代版本 webpack ,它的出现极大的解决了现有的问题。webpackk4.x可以不使用 webpack.config.js 配置文件可以使用下面6小步完成项目的构建:创建一个项目目录(webpack-demo),然后进入改目录mkdir webpack-demo && cd webpack-demo初始化 package.json 文件npm init -y加载 webpack 和 webpack-cli 依赖npm install webpack webpack-cli –save-dev在项目中添加 /src/index.js 文件(index.js 是默认的入口文件,默认入口目录为/src,当然你也可以自定义入口文件,需要修改 package.json 中的 main 配置项为指定的文件)index.js 文件代码如下:console.log(‘hello webpack.’)打开 package.json 在 scripts 配置项中添加如下代码:“scripts”: { “build”: “webpack”}注:这就是NPM的 scripts 命令运行 npm run build 命令,之后在项目中你将看到一个 ~/dist/main.js 的文件。在命令窗口你因该注意到如下的警告提示: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/concepts/mode/忽略这条提示信息,我们发现webpack4.x的项目初始化配置和webpack3.x没什么大的区别,但是webpack4.x少了必须要的 webpack.config.js 配置文件。打包模式的改变我们再回头查看上面这个提示信息,它的意思就是说:‘如果没有设置打包模式这个配置项,那么默认的打包模式为生产模式(production),而对于开发模式(development),需要配置 mode 配置项’,说到这里,我想各位看官应该明白了webpack4.x增加了很多默认配置项,针对不了解webpack的人员或小应用开发的场景,这样做无异省时省力。但实际应用中,我们往往还是区分开发模式和生产模式,但这在webpack4.x中也不是什么难事儿,只要修改 package.json 中的 scripts 如下:“scripts”: { “dev”: “webpack –mode development”, // 用于开发模式 “build”: “webpack –mode production” // 用于生产模式}‘对!webpack4.x就是这么简单’。我们不需要像webpack3.x那样分别定义开发模式和生产模式这样两份配置文件。重载默认的配置项入口/出口没有了配置文件 webpack.config.js ,在减少了我们的配置工作量同时,也给初窥门径的我们带来了一些疑问。例如:如何自定义入口/出口?在没有 webpack.config.js 的情况下,我们可以在命令行中添加入口/出口配置项,代码如下:“scripts”: { “dev”: “webpack –mode development ./src/entry.js –output ./dist/bundle.js”, // 用于开发模式 “build”: “webpack –mode production ./src/entry.js –output ./dist/bundle.min.js” // 用于生产模式}这只是不使用 webpack.config.js 的一种方案。以上就是webpack4.x给我们带来的整体变化。但是原来 webpack.config.js 配置文件中的 module 和 plugins 配置项中的功能实现还是需要使用 webpack.config.js。虽然webpack团队的计划是 0 配置一些常用的loader,plugin,但实现的仅有 UglifyJSPlugin 内置插件,在生产模式无需引入它就可以实现 *.js 代码的压缩。其它的loader和plugin则只能通过 webpack.config.js 来引入。webpack的迁移和注意事项看到webpack4.x的这些变化,很多人不仅会问webpack3.x到webpack4.x的迁移是不是很麻烦,其实并不麻烦,webpack4.x向后兼容webpack.3x。前面为了不引入 webpack.config.js ,我们使用了npm的 scripts ,其时像入口/出口的重载,我们也可以在 webpack.config.js 配置文件中完成,配置跟原来的相似,但是webpack4.x有如下问题需要注意:升级到webpack4.x,你会发现在使用 extract-text-webpack-plugin 分离 *.css 出文件时经常出错,这是 extract-text-webpack-plugin 本身的问题,官方推荐使用 mini-css-extract-plugin 来避免问题的出现,但使用 mini-css-extract-plugin 有一个限制就是webapck须是4.2.0版本以上(较低的版本不支持)。使用 使用babel-loader 转化ES6->ES5时将不需要 .babelrc 配置文件,你只需要在 package.json 的 scripts 中添加 –module-bind js=babel-loader 即可完成对 babel-loader 的配置。其他的loader和plugin没有什么大的变化。其实讲到这里基本完了,下面是用webpack4.x构建的一个demo。webpack4.x的demo紧接上面的配置:首先,添加 html-wepback-plugin 和 html-loader 依赖:npm install html-webpack-plugin html-loader –save-devhtml-webpack-plugin 生成html文件(html文件用来加载打包生成 bundle.js 文件),当然你也可以使用webpack支持的各种模板loader,这里使用 html-loader 支持的 *.html 类型模板来生成。其次,添加 mini-css-extract-plugin 和 css-loader 依赖:npm install mini-css-extract-plugin css-loader –save-devloader和plugin配置与webpack3.x类同,也可参考下面提供代码中的 webpack.config.js 文件。然后,添加 babel-loader 、@babel/babel-core 和 @babel/babel-preset 依赖:npm install @babel/core babel-loader @babel/preset-env –save-devloader和plugin配置与webpack3.x类同,也可参考下面提供源码中的 webpack.config.js 文件。修改 package.json 中 scripts 如下:“scripts”: { “dev”: “webpack –mode development –module-bind js=babel-loader ./src/entry.js –output ./dist/bundle.js”, “build”: “webpack –mode production ./src/entry.js –module-bind js=babel-loader –output ./dist/bundle.min.js”},最后,添加 webpack-dev-server 依赖,实现项目文件修改,浏览器及时刷新npm install webpack-dev-server在 package.json 中 scripts 的 dev 替换 webpack 为 webpack-dev-server 即可,代码如下:“scripts”: { “dev”: “webpack-dev-server –mode development –module-bind js=babel-loader ./src/entry.js –output ./dist/bundle.js”, “build”: “webpack –mode production ./src/entry.js –module-bind js=babel-loader –output ./dist/bundle.min.js”},这样一个简单的demo就完成了。其他的loader和plugin配置和webpack3.x类同。wepback4-demo源代码参考资料webpack-tutorialwebpack官方指南我的webpack系列文章 ...

October 18, 2018 · 2 min · jiezi

多页应用 Webpack4 配置优化与踩坑记录

前言最近新起了一个多页项目,之前都未使用 webpack4,于是准备上手实践一下。这篇文章主要就是一些配置介绍,对于正准备使用 webpack4 的同学,可以做一些参考。webpack4 相比之前的 2 与 3,改变很大。最主要的一点是很多配置已经内置,使得 webpack 能“开箱即用”。当然这个开箱即用不可能满足所有情况,但是很多以往的配置,其实可以不用了。比如在之前,压缩混淆代码,需要增加uglify插件,作用域提升(scope hosting)需要增加ModuleConcatenationPlugin。而在 webpack4 中,只需要设置 mode 为 production即可。当然,如果再强行增加这些插件也不会报错。所以我建议,如果大家想迁移到 webpack4,还是从 0 开始做加法,参考历史,重新做一个配置。而不是从历史的配置里删删减减,再升级为 webpack4。这样 webpack4 的配置会显得更精简。打包优化打包优化主要就是多页应用构建时,对所有页面加载的依赖进行合理打包。这个目前业界都已经有了很多实践,包括 webpack4,也有很多文章介绍。我再补充几个不容易注意的小细节。有些点我不详细介绍,不熟悉 webpack 配置的同学可能会不明白,可以搜索对应关键词,网上肯定有非常详细的文章介绍。首先,构建多页应用,往往会抽离如下几个 chunk 包:common:将被多个页面同时引用的依赖包打到一个 common chunk 中。网上大部分教程是被引入两次即打入 common。我建议可以根据自己页面数量来调整,在我的工程中,我设置引入次数超过页面数量的 1/3 时,才会打入 common 包。dll: 将每个页面都会引用的且基本不会改变的依赖包,如 react/react-dom 等再抽离出来,不让其他模块的变化污染 dll 库的 hash 缓存。manifest: webpack 运行时(runtime)代码。每当依赖包变化,webpack 的运行时代码也会发生变化,如若不将这部分抽离开来,增加了 common 包 hash 值变化的可能性。页面入口文件对应的page.js然后我们会给打出的 chunk 包名,注入 contentHash,以实现最大缓存效果。在我们分 chunk 的过程中,最关键的一个思想就是,每次迭代发布,尽量减少 chunk hash 值的改变。这个在业界也有很多非常多的实践,比如这篇文章:https://github.com/pigcan/blo…不过在 webpack4 中,我们不用再增加这么多插件啦,一个 optimization 配置完全就能搞定。我先贴上我的 webpack 的 optimization 配置,然后我再对其做一些介绍,加深大家印象const commonOptions = { chunks: ‘all’, reuseExistingChunk: true}export default { namedChunks: true, moduleIds: ‘hashed’, runtimeChunk: { name: ‘manifest’ }, splitChunks: { maxInitialRequests: 5, cacheGroups: { polyfill: { test: /[\/]node_modules\/[\/]/, name: ‘polyfill’, priority: 2, …commonOptions }, dll: { test: /[\/]node_modules[\/][\/]/, name: ‘dll’, priority: 1, …commonOptions }, commons: { name: ‘commons’, minChunks: Math.ceil(pages.length / 3), // 至少被1/3页面的引入才打入common包 …commonOptions } } }}runtimeChunk在 webpack4 之前,抽离 manifest,需要使用 CommonsChunkPlugin,配置一个指定 name 属性为’manifest’的 chunk。在 webpack4 中,无需手动引入插件,配置 runtimeChunk 即可。splitChunks这个配置能让我们以一定规则抽离想要的包,我们可能会抽好几个包,如 verdor + common,所以 splitChunks 中提供 cacheGroups 字段,cacheGroups 每增加一个 key,就相当于多一个抽包规则。在网上很多教程中,dll 往往是专门再加一个 webpack 配置,使用 DllPlugin 来构建 dll 库,再在自己项目工程的 webpack 中利用 DllReferencePlugin 来映射 dll 库。虽然这样构建速度会快不少,但是,哎,是真 TM 烦…..我是一个很怕烦的人,我情愿在 webpack4 中利用 splitChunks,配好规则,再抽离对应的 dll 包。当然这个大家可以自己根据实际情况选择方案。除了 dll 与 common 两个 chunk,我还加了一个 polyfill。这是因为我们用的某些新的库或者使用某些 ES6+语法(如 async/await)需要 runtime 垫片。比如我工程中使用了 react16,需要增加Map/Set/requestAnimationFrame (https://reactjs.org/docs/java…。那我必须在 dll 库加载之前增加 polyfill,因此我将所有 core-js 与 babel 引入的包专门打进 polyfill,保证后续加载的 chunk 能执行。priority字段用来配置 chunk 的引入优先级,一般的项目应该都是 polyfill > dll > common > page。splitChunks 中配置项maxInitialRequests表示在一个入口(entry)中,最大初始请求 chunk 数(不包含按需加载的,即 dom 中 script 引入的 chunk),默认值是 3。我现在 cacheGroups 中已经有三个,又因为配置了 runtimeChunk,会打出 manifest,故而总共有 4 个 chunk 包,超出了默认 3 个,因此需要重新配置值。moduleIds稍微了解过 webpack 运行机制的同学会知道,项目工程中加载的 module,webpack 会为其分配一个 moduleId,映射对应的模块。这样产生的问题是一旦工程中模块有增删或者顺序变化,moduleId 就会发生变化,进而可能影响所有 chunk 的 content hash 值。只是因为 moduleId 变化就导致缓存失效,这肯定不是我们想要的结果。在 webpack4 以前,通过 HashedModuleIdsPlugin 插件,我们可以将模块的路径映射成 hash 值,来替代 moduleId,因为模块路径是基本不变的,故而 hash 值也基本不变。但在 webpack4 中,只需要optimization的配置项中设置 moduleIds 为 hashed 即可。namedChunks除了 moduleId,我们知道分离出的 chunk 也有其 chunkId。同样的,chunkId 也有因其 chunkId 发生变化而导致缓存失效的问题。由于manifest与打出的 chunk 包中有chunkId相关数据,所以一旦如“增删页面”这样的操作导致 chunkId 发生变化,可能会影响很多的 chunk 缓存失效。在 webpack4 以前,通过增加NamedChunksPlugin,使用 chunkName 来替换 chunkId,实现固化 chunkId,保持缓存的能力。在 webpack4 中,只需在optimization的配置项中设置 namedChunks 为 true 即可。css 相关在 webpack4 以前,使用 extract-text-webpack-plugin 插件将 css 从 js 包中分离出来单独打包。在 webpack 中则需要换成 MiniCssExtractPlugin。并且在生产环境或者需要 HMR(模块热替换)时,要用 MiniCssExtractPlugin.loader 替换 style-loader。注意,这里有个坑。由于开发环境我们会配置热更新,css 的热更新目前MiniCssExtractPlugin.loader自身还待支持,故而还需要增加 css-hot-loader。 切记,css-hot-loader一定不能在生产环境下使用。否则每次构建过程所有 js chunk 包的 contentHash 值都会不一致,进而导致所有 js 缓存失效。 因为生产环境增加这个配置不会有任何报错,页面也能正常构建,故而容易忽视。简化多页应用的入口文件使用react/vue等框架的同学知道,我们一般需要一个入口index.js,如这样:import React from ‘react’import ReactDOM from ‘react-dom’import App from ‘./app’ReactDOM.render(<App />, document.getElementById(‘root’))如果你还需要使用dva,或者给所有 react 页面增加一个 layout 功能的话,可能就会变成这样:import React from ‘react’import dva from ‘dva’import Model from ‘./model’import Layout from ‘~@/layout’import App from ‘./app’const app = dva()app.router(() => ( <Layout> <App /> </Layout>))app.model(Model)app.start(document.getElementById(‘root’))如果每个页面都这样,略略有点儿难受,因为程序员最怕写重复的东西了。但是它又必须要有,没办法抽离成一个单独文件。因为这个是入口文件,而多页工程,每个页面必须要有自己的入口文件,即使他们长得一模一样。于是,我们的资源目录就会是这样:- src - layout.js - pages - pageA - index.js - app.js - model.js - pageB - index.js - app.js - model.js因为所有的 index 都一样,我理想中的页面的入口文件仅仅需要app.js就好,像这样:- src - layout.js - pages - pageA - app.js - model.js - pageB - app.js - model.js作为一名前端开发工程师,Node 对于我们来说,应该是熟练运用的工具,而不是仅仅拿别人已经封装好的各类工具。在这个问题中,我们大可以在 webpack 构建前,通过Node的文件系统(File System),对应我们的每个页面,通过同一个入口文件模板,创建一些临时入口文件:- src - .entires - pageA.js - pageB.js - layout.js - pages然后将这些临时文件,作为 webpack 的 entry 配置。代码如下:const path = require(‘path’)const fs = require(‘fs’)const glob = require(‘glob’)const rimraf = require(‘rimraf’)const entriesDir = path.resolve(process.cwd(), ‘./src/.entries’)const srcDir = path.resolve(process.cwd(), ‘./src’)// 返回webpack entry配置module.exports = function() { if (fs.existsSync(entriesDir)) { rimraf.sync(entriesDir) } fs.mkdirSync(entriesDir) return buildEntries(srcDir)}function buildEntries(srcDir) { return getPages(srcDir).reduce((acc, current) => { acc[current.pageName] = buildEntry(current) return acc }, {})}// 获取页面数据,只考虑一级目录function getPages(srcDir) { const pagesDir = ${srcDir}/pages const pages = glob.sync(${pagesDir}/**/app.js) return pages.map(pagePath => { return { pageName: path.relative(pagesDir, p).replace(’/app.js’, ‘’), // 取出page文件夹名 pagePath: pagePath } })}// 构建临时入口文件function buildEntry({ pageName, pagePath }) { const fileContent = buildFileContent(pagePath) const entryPath = ${entriesDir}/${pageName}.js fs.writeFileSync(entryPath, fileContent) return entryPath}// 替换模板中的 App 模块地址,返回临时入口文件内容function buildFileContent(pagePath) { return import React from 'react' import dva from 'dva' import Model from './model' import Layout from '~@/layout' import App from 'PAGE_APP_PATH' const app = dva() app.router(() =&gt; ( &lt;Layout&gt; &lt;App /&gt; &lt;/Layout&gt; )) app.model(Model) app.start(document.getElementById('root')) .replace(PAGE_APP_PATH, pagePath)}这样一来,我们就简单的去掉了重复的入口文件,还增加了一个 layout 的功能。这只是简单的代码,实际项目可能还有多级目录,多个 model 等等,需要自己再定制啦。webpack4出来已经挺久了,文章写的有点儿滞后了,所以很多我觉得应该大家都明白的地方就没详细写了。如果还有什么疑问的话,欢迎评论~~ ...

October 15, 2018 · 3 min · jiezi

webpack 4.17.1管理输出

到目前为止,我们在 index.html 文件中手动引入所有资源,然而随着应用程序增长,并且一旦开始对文件名使用哈希(hash)]并输出多个 bundle,手动地对 index.html 文件进行管理,一切就会变得困难起来。然而,可以通过一些插件,会使这个过程更容易操控。预先准备调整项目结构src/print.jsexport default function printMe() { console.log(‘I get called from print.js!’);}src/index.jsimport _ from ’lodash’import printMe from ‘./print.js’function component () { var ele = document.createElement(‘div’) var btn = document.createElement(‘button’) ele.innerHTML = _.join([‘hello’, ‘webpack’], ’ ‘) btn.innerHTML = ‘click me and check the console’ btn.onclick = printMe ele.appendChild(btn) return ele;}document.body.appendChild(component());修改dist/index.html 为webpack 分离入口做好准备<!doctype html><html> <head> <title>Output Management</title> <script src="./print.bundle.js"></script> </head> <body> <script src="./app.bundle.js"></script> </body></html>修改webpack.config.js 新增一个print入口起点,然后修改 output,以便根据入口起点名称动态生成 bundle 名称:const path = require(‘path’)module.exports = { entry: { app: ‘./src/index.js’, print: ‘./src/print.js’ } output: { filename: ‘[name].bundle.js’ // __dirname表示当前文件所在的目录的绝对路径 path: path.resolve(__dirname, ‘dist’) }};执行构建命令cnpm run build在浏览器中打开index.html点击按钮,。。。但是,如果我们更改了我们的一个入口起点的名称,甚至添加了一个新的名称,会发生什么?生成的包将被重命名在一个构建中,但是我们的index.html文件仍然会引用旧的名字。我们用 HtmlWebpackPlugin 来解决这个问题。设定 HtmlWebpackPlugin安装插件cnpm install –save-dev html-webpack-plugin修改webpack.config.jsconst path = require(‘path’)const HtmlWebpackPlugin = require(‘html-webpack-plugin’) // 引入htmwebpackplugin插件module.exports = { entry: { app: ‘./src/index.js’, print: ‘./src/print.js’ }, plugins: [ new HtmlWebpackPlugin({// 使用htmwebpackplugin插件 title: ‘Output Management’ }) ], output: { filename: ‘[name].bundle.js’, // __dirname表示当前文件所在的目录的绝对路径 path: path.resolve(__dirname, ‘dist’) }};再次构建 cnpm run build之前在dist文件夹中有一个我们自己建的index.html文件,我们为这个文件手动引入bundle,使用htmlwebpackplugin之后,会默认生成一个index.html文件,替换掉原来我们手动添加的index.html文件,并且文件中会自动添加所有的bundle<!DOCTYPE html><html> <head> <meta charset=“UTF-8”> <title>Output Management</title> </head> <body> <script type=“text/javascript” src=“app.bundle.js”></script><script type=“text/javascript” src=“print.bundle.js”></script></body></html>清理dist文件夹你可能已经注意到,由于过去的指南和代码示例遗留下来,导致我们的 /dist 文件夹相当杂乱。webpack 会生成文件,然后将这些文件放置在 /dist 文件夹中,但是 webpack 无法追踪到哪些文件是实际在项目中用到的。通常,在每次构建前清理 /dist 文件夹,是比较推荐的做法,因此只会生成用到的文件。让我们完成这个需求。安装clean-webpack-plugin 是一个比较普及的管理插件cnpm install clean-webpack-plugin –save-dev修改webpack.config.js文件const path = require(‘path’)const HtmlWebpackPlugin = require(‘html-webpack-plugin’) // 引入htmwebpackplugin插件const CleanWebpackPlugin = require(‘clean-webpack-plugin’) // 引入CleanWebpackPlugin插件module.exports = { entry: { app: ‘./src/index.js’, print: ‘./src/print.js’ }, plugins: [ new CleanWebpackPlugin([‘dist’]), new HtmlWebpackPlugin({// 使用htmwebpackplugin插件 title: ‘Output Management’ }) ], output: { filename: ‘[name].bundle.js’, // __dirname表示当前文件所在的目录的绝对路径 path: path.resolve(__dirname, ‘dist’) }};执行cnpm run build 查看dist文件夹,就只有构建后新生成的文件了Manifest ...

October 1, 2018 · 1 min · jiezi

webpack4.17.1模块热替换

模块热替换(Hot Module Replacement 或 HMR)是 webpack 提供的最有用的功能之一。它允许在运行时更新各种模块,而无需进行完全刷新。启用HMR启用此功能实际上相当简单。而我们要做的,就是更新 webpack-dev-server 的配置,和使用 webpack 内置的 HMR 插件。我们还要删除掉 print.js 的入口起点,因为它现在正被 index.js 模式使用。修改webpack.config.js…const webpack = require(‘webpack’)module.exports = { entry: { app: ‘./src/index.js’ }, devtool: ‘inline-source-map’, devServer: { contentBase: ‘/dist’, hot: true }, plugins: [ new CleanWebpackPlugin([‘dist’]), new HtmlWebpackPlugin({// 使用htmwebpackplugin插件 title: ‘Output Management’ }), new webpack.NamedModulesPlugin(), // 以便更容易查看要修补(patch)的依赖。 new webpack.HotModuleReplacementPlugin() ],…};注意,我们还添加了 NamedModulesPlugin,以便更容易查看要修补(patch)的依赖。在起步阶段,我们将通过在命令行中运行 npm start 来启动并运行 dev server。现在,我们来修改 index.js 文件,以便当 print.js 内部发生变更时可以告诉 webpack 接受更新的模块。修改index.jsimport _ from ’lodash’import printMe from ‘./print.js’function component () { var ele = document.createElement(‘div’) var btn = document.createElement(‘button’) ele.innerHTML = _.join([‘hello’, ‘webpack’, ‘world’], ’ ‘) btn.innerHTML = ‘click me and check the console’ btn.onclick = printMe ele.appendChild(btn) return ele;}document.body.appendChild(component());if (module.hot) { module.hot.accept(’./print.js’, function () { console.log(‘accepting the updated printMe module!’); printMe() })}随意修改print.js浏览器中可以看到实时变化通过 Node.js APIHMR 修改样式表安装stylee-loader css-loadercnpm install –save-dev style-loader css-loader修改webpack.config.jsconst path = require(‘path’)const HtmlWebpackPlugin = require(‘html-webpack-plugin’) // 引入htmwebpackplugin插件const CleanWebpackPlugin = require(‘clean-webpack-plugin’) // 引入CleanWebpackPlugin插件const webpack = require(‘webpack’)module.exports = { entry: { app: ‘./src/index.js’ }, devtool: ‘inline-source-map’, devServer: { contentBase: ‘/dist’, hot: true }, module: { rules: [{ test: /.css$/, use: [‘style-loader’, ‘css-loader’] }] }, plugins: [ new CleanWebpackPlugin([‘dist’]), new HtmlWebpackPlugin({// 使用htmwebpackplugin插件 title: ‘Output Management’ }), new webpack.NamedModulesPlugin(), // 以便更容易查看要修补(patch)的依赖。 new webpack.HotModuleReplacementPlugin() ], output: { filename: ‘[name].bundle.js’, // __dirname表示当前文件所在的目录的绝对路径 path: path.resolve(__dirname, ‘dist’), publicPath: ‘/’ }};cnpm start启动服务修改样式文件 ...

October 1, 2018 · 1 min · jiezi

webpack4.17.1管理资源

通过 webpack 来管理资源,例如图片、字体webpack 最出色的功能之一就是,除了 JavaScript,还可以通过 loader 引入任何其他类型的文件。也就是说,以上列出的那些 JavaScript 的优点(例如显式依赖[import引入]),同样可以用来构建网站或 web 应用程序中的所有非 JavaScript 内容。让我们从 CSS 开始起步修改dist/index.html<!doctype html><html> <head> <title>Asset Management</title> </head> <body> <script src="./bundle.js"></script> </body></html>在浏览器中打开index.html就可以看到"Hello webpack" yeah加载 CSS为了从 JavaScript 模块中 import 一个 CSS 文件,你需要在 module 配置中 安装并添加 style-loader 和 css-loader:cnpm install –save-dev style-loader css-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: [ // 并将其提供给指定的 loader ‘style-loader’, ‘css-loader’ ] } ] }};这使你可以在依赖于此样式的文件中 import ‘./style.css’。现在,当该模块运行时,含有 CSS 字符串的 <style> 标签,将被插入到 html 文件的 <head> 中添加一个style.css文件修改src/index.js 将style.css引入index.js中import _ from ’lodash’import ‘./style.css’ // 引入cssfunction component() { var element = document.createElement(‘div’); element.innerHTML = _.join([‘Hello’, ‘webpack’], ’ ‘); element.classList.add(‘hello’); // 使用css文件中的hello样式 return element;}document.body.appendChild(component());运行构建命令 cnpm run build浏览器中打开index.html 源代码中没有包含样式,只有在审查元素可以看到加载图片安装 file-loadercnpm install –save-dev file-loader修改webpack.config.jsconst path = require(‘path’);module.exports = {… module: { rules: [ { test: /.css$/, // 根据正则表达式,来确定应该查找哪些文件 use: [ // 并将其提供给指定的 loader ‘style-loader’, ‘css-loader’ ] }, { test: /.(png|svg|jpg|gif)$/, use: [ ‘file-loader’ ] } ] }};添加一张图片修改index.jsimport _ from ’lodash’import ‘./style.css’import Icon from ‘./jxb.png’function component() { var element = document.createElement(‘div’); element.innerHTML = _.join([‘Hello’, ‘webpack’], ’ ‘); element.classList.add(‘hello’); // 使用css文件中的hello样式 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.png’);}再次构建,运行构建命令 cnpm run build查看文件目录,dist文件夹中多了一张图片浏览器中打开index.html合乎逻辑下一步是,压缩和优化你的图像。查看 image-webpack-loader 和 url-loader,以了解更多关于如果增强加载处理图片功能。加载字体file-loader 和 url-loader 可以接收并加载任何文件,然后将其输出到构建目录。这就是说,我们可以将它们用于任何类型的文件,包括字体修改webpack.config.jsconst path = require(‘path’);module.exports = {… module: { rules: [ { test: /.css$/, // 根据正则表达式,来确定应该查找哪些文件 use: [ // 并将其提供给指定的 loader ‘style-loader’, ‘css-loader’ ] }, { test: /.(png|svg|jpg|gif)$/, use: [ ‘file-loader’ ] },{ test: /.(woff|woff2|eot|ttf|otf)$/, use: [ ‘file-loader’ ] } ] }};在项目中添加一些字体通过一个 @font-face 声明引入。本地的 url(…) 指令会被 webpack 获取处理,就像它处理图片资源一样修改src/style.css@font-face { font-family: ‘MyFont’; src: url(’./my-font.woff2’) format(‘woff2’), url(’./my-font.woff’) format(‘woff’); font-weight: 600; font-style: normal;}.hello { color: red; background: url(’./jxb.jpg’); font-family: ‘MyFont’;}再次构建,运行构建命令cnpm run build加载数据如 JSON 文件,CSV、TSV 和 XML。类似于 NodeJS,JSON 支持实际上是内置的,也就是说 import Data from ‘./data.json’ 默认将正常运行。要导入 CSV、TSV 和 XML,你可以使用 csv-loader 和 xml-loader。修改webpck.config.jsconst path = require(‘path’)module.exports = {… module: { rules: [{ test: /.(csv|tsv)$/, use: [‘csv-loader’] }, { test: /.xml$/, use: [‘xml-loader’] }] }};在源目录src中添加一个data.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.js…import Data from ‘./data.xml’function component () { var ele = document.createElement(‘div’) ele.innerHTML = _.join([‘hello’, ‘webpack’], ’ ‘) … console.log(Data) return ele;}document.body.appendChild(component());再次构建cnpm run build构建成功,浏览器打开dist/index.html全局资源上述所有内容中最出色之处是,以这种方式加载资源,你可以以更直观的方式将模块和资源组合在一起。无需依赖于含有全部资源的 /assets 目录,而是将资源与代码组合在一起 ...

October 1, 2018 · 2 min · jiezi

优秀前端必知的话题:我们应该做些力所能及的优化

在 Web 应用开发过程中,我们经常谈及到的就是优化,而优化往往又是既简单而又复杂的过程,优化这个命题很广,最终体现出来的都是用户体验问题,我们一切优化都是为了用户体验。为什么说简单?在现代 Web 开发生态中,有非常优秀的工具链帮助我们做一些很实际的优化工作,例如 webpack 。这些工具可以很好的帮助我们解决包之间的依赖、减小包大小、提取公共模块等等问题。为什么说复杂?优化这个话题我们谈了很多年,只要有用户群,优化问题就会一直存在。而优化工作涉及的领域特别广,包含的因素又特别多,有时候需要针对特定的场景做特殊的优化工作,所以说又很复杂。不管是简单还是复杂,作为程序员,我们应当做一些我们力所能及的优化工作,本文属于探讨性话题,希望广大网友能够在留言区留下您的一些思考。Code Splitting这里不探讨如何书写高性能的代码,而是探讨下我们书写的代码该如何被构建。这里以 webpack 为构建工具(版本为4.19),来阐述下在 webpack 我们该做的优化工作。webpack 从 v4 版本开始,做了很多的优化工作,详情请看这里 。我们就拿 Code Splitting 说起,Code Splitting 是 webpack 一项重要的编译特性,能够帮助我们将代码进行拆包,抽取出公共代码。利用这项特性我们可以做更多的优化工作,减少加载时间,例如可以做按需加载。而在 webpack 中开启 Code Splitting 也很简单,它是一项开箱即用的插件,示例代码如下:module.export = {// …optimization: {splitChunks: {chunks: ‘all’}}}上面的 chunks 配置建议大家配置为 all ,详细配置请参考:splitChunks.chunks这里给出个参考结果:分别为配置前和配置后这里明显多出了几个包含 vendors~ 字样的文件,而且你会发现 app 这个文件被提取出了 vendorsappreact_vendor 和 vendors_app_redux_vendor 这两个文件。至于最大的文件为什么还有1.02M,我们可以使用 analyze 来分析下包的结构,这里是由于包含了 antd 的文件。在实际开发过程中,路由也是需要进行 Code Splitting ,在过去我们经常使用 bundle-loader ,来帮助我们进行代码分割,它是基于 require.ensure 接口进行实现。既然我们可以对路由进行代码分割,那么路由页面中的组件我们是否可以按需加载,实现代码分割呢?答案是显然的。这种业务场景也是非常的多,这里我举一个例子,就是一个登录页面,登录有多种方式,其中最常见的就是账号登录和扫码登录,默认为扫码登录。当用户没有选择账号登录,那么按道理这部分代码我们可以不进行加载,从而减少加载时间,优化用户体验。我们建议能进行组件级分割就分割,最大化减小页面大小。在 React 中虽然也可以使用 bundle-loader 来实现组件级代码分割,但是也会有一些问题。在后来,React Router 官方也推荐使用 react-loadable 来进行代码分割。强烈建议 React 使用者使用此库,该库的功能很强大,是基于 import() 实现。它可以实现预加载、重新加载等等强大功能。Tree Shaking如果你对自己编写的代码很了解,你可以通过在 package.json 中添加 sideEffects 来启用 Tree Shaking ,即摇树优化,帮助我们删掉一些不用的代码。这里不再赘述,详情可以点击Tree Shaking。Dynamic import在谈到 Code Spliting 时,我们不得不想到 dynamic import ,在之前版本的 webpack 中,我们想实现动态加载使用的是 require.ensure ,而在新版本中,取而代之的 import() ,这是TC39关于使用 import()的提案,而目前 import()兼容性如下:import() 返回一个 Promise ,如果你想使用它请确保支持 Promise 或者使用 Polyfill ,在想使用 import() 前,我们还得使用预处理器,我们可以使用 @babel/plugin-syntax-dynamic-import 插件来帮助webpack解析。webpack 官方给了我们一个 dynamic import 的示例 ,这里我就不做举例。使用 import() 我们可以很方便的实现 preload 预加载、懒加载以及上面谈到的 Code Splitting。PolyfillPolyfill 现在对于大家来说应该并不陌生,他可以帮助我们使用一些浏览器目前并不支持的特性,例如 Promise 。在Babel中,官方建议使用 babel-preset-env 配合 .browserslistrc ,开发人员可以无需关心目标环境,提升开发体验。尤其在 Polyfill 方面,只要我们配置好 .browserslistrc ,Babel 就可以智能的根据我们配置的浏览器列表来帮助我们自注入 Polyfill ,比如:.babelrc{“presets”: [["@babel/preset-env",{“useBuiltIns”: “entry”}]]}useBuiltIns 告诉 babel-preset-env 如何配置 Polyfill ,这里我配置为:entry ,然后在 webpack 入口文件中引入 import ‘@babel/polyfill’ 即可,这里注意不能多次引入 import ‘@babel/polyfill’ ,否则会报错。.browserslistrc> 1%Last 2 versions这样就完成了自动根据 .browserslistrc注入 Polyfill ,但是这样有一个问题,就是所有的浏览器都会有 Polyfill 的并集。每个浏览器之间的特性具有很大的差异,为了尽可能的减小包的大小,我们可以为每个主流浏览器单独生成 Polyfill ,不同的浏览器加载不同的 Polyfill 。首屏文件SPA 程序打包出来的html文件一般都是很小的,也就2kb左右,似乎我们还可以利用下这个大小做个优化,有了解初始拥塞窗口 的同学应该知道,通常是14.6KB,也就意味着这我们还能利用剩下的12KB左右的大小去干点什么,这了我建议内联一些首屏关键的css文件(可以使用 criticalCSS ),或者将css初始化文件内联进去,当然你也可以放其他东西,这里只是充分利用下初始拥塞窗口 特性。这里顺便讲下css初始化,css初始化有很多种选择,其中有三种比较出名的,分别是:normalize.css 、sanitize.css 和 reset.css 。关于这三种的区别我就直接引用了。normalize.css and sanitize.css correct browser bugs while carefully testing and documenting changes. normalize.css styles adhere to css specifications. sanitize.css styles adhere to common developer expectations and preferences. reset.css unstyles all elements. Both sanitize.css and normalize.css are maintained in sync.缓存在利用 webpack 打包完之后,我们有些文件几乎不会变更,比如我这里列举的react、redux、polyfill相关的文件。entry: {react_vendor: [‘react’, ‘react-dom’, ‘react-router-dom’],redux_vendor: [‘react-redux’,‘redux’, ‘redux-immutable’,‘redux-saga’, ‘immutable’],polyfill: ‘@babel/polyfill’,app: path.join(process.cwd(),‘app/app.js’)}这些不变的文件我们就可以好好的利用下,常见(http 1.1)的就是设置 Etag ,Last-Modified 和 Cache-Control 。前面两种属于对比缓存,还是需要和服务器通信一次,只有当服务器返回 304 ,浏览器才会去读取缓存文件。而 Cache-Control 属于强制缓存,服务器设定 max-age 当过了设定的时间后才会向服务器发起请求。这里打包再配上 chunk-hash 几乎可以完美的配置缓存。当然还可以利用 localStorage 来做缓存,这里提出一种思路,是我以前在效仿百度首页缓存机制想的。我们可以在把js文件版本号弄成一个配置,同时存储在服务端和客户端,比如:{“react_version”: 16.4,“redux_version”: 5.0.6,“web_version”: 1.0}客户端将该版本号存储在 cookie 或其他存储引擎中,这里推荐 localForage 来做存储。服务端将最新版本号渲染到html文件中,然后通过js脚本对比版本号,如若版本号不同,则进行加载对应的js文件,加载成功后再存储到本地存储中。如果相同,则直接取本地存储文件。还有一种缓存的场景,就是有一些api服务端更新的进度很慢,比如一天之内访问的数据都是一样的,这样就可以对客户端进行请求缓存并拦截请求,从而优化速度,减小服务器压力。其他还有其他很多可以优化的地方,比如减少http请求、图片懒加载等等,就不一一列举了,大家可以看雅虎34条军规:尽量减少 HTTP 请求个数——须权衡使用 CDN(内容分发网络)为文件头指定 Expires 或 Cache-Control ,使内容具有缓存性。避免空的 src 和 href使用 gzip 压缩内容把 CSS 放到顶部把 JS 放到底部避免使用 CSS 表达式将 CSS 和 JS 放到外部文件中减少 DNS 查找次数精简 CSS 和 JS避免跳转剔除重复的 JS 和 CSS配置 ETags使 AJAX 可缓存尽早刷新输出缓冲使用 GET 来完成 AJAX 请求延迟加载预加载减少 DOM 元素个数根据域名划分页面内容尽量减少 iframe 的个数避免 404减少 Cookie 的大小使用无 cookie 的域减少 DOM 访问开发智能事件处理程序用 <link> 代替 @import避免使用滤镜优化图像优化 CSS Spirite不要在 HTML 中缩放图像——须权衡favicon.ico要小而且可缓存保持单个内容小于25K打包组件成复合文本写在最后关于优化的文章网上太多太多,这篇文章并不是告诉大家如何优化,而是在平时写代码时能够培养一种习惯、一种意识,就是做我们力所能及的优化以及要知其所以然。文 / GoDotDotDotLESS is MORE编 / 荧声本文已由作者授权发布,版权属于创宇前端。欢迎注明出处转载本文。本文链接:欢迎点赞、收藏、留言评论、转发分享和打赏支持我们。打赏将被完全转交给文章作者。感谢您的阅读。 ...

September 30, 2018 · 2 min · jiezi

webpack使用-详解DllPlugin

前言(时光飞逝,转眼又偷懒了一个多月)什么是DLLDLL(Dynamic Link Library)文件为动态链接库文件,在Windows中,许多应用程序并不是一个完整的可执行文件,它们被分割成一些相对独立的动态链接库,即DLL文件,放置于系统中。当我们执行某一个程序时,相应的DLL文件就会被调用。举个例子:很多产品都用到螺丝,但是工厂在生产不同产品时,不需要每次连带着把螺丝也生产出来,因为螺丝可以单独生产,并给多种产品使用。在这里螺丝的作用就可以理解为是dll。为什么要使用Dll通常来说,我们的代码都可以至少简单区分成业务代码和第三方库。如果不做处理,每次构建时都需要把所有的代码重新构建一次,耗费大量的时间。然后大部分情况下,很多第三方库的代码并不会发生变更(除非是版本升级),这时就可以用到dll:把复用性较高的第三方模块打包到动态链接库中,在不升级这些库的情况下,动态库不需要重新打包,每次构建只重新打包业务代码。还是上面的例子:把每次构建,当做是生产产品的过程,我们把生产螺丝的过程先提取出来,之后我们不管调整产品的功能或者设计(对应于业务代码变更),都不必重复生产螺丝(第三方模块不需要重复打包);除非是产品要使用新型号的螺丝(第三方模块需要升级),才需要去重新生产新的螺丝,然后接下来又可以专注于调整产品本身。基本用法使用dll时,可以把构建过程分成dll构建过程和主构建过程(实质也就是如此),所以需要两个构建配置文件,例如叫做webpack.config.js和webpack.dll.config.js。1. 使用DLLPlugin打包需要分离到动态库的模块DllPlugin是webpack内置的插件,不需要额外安装,直接配置webpack.dll.config.js文件:module.exports = {= entry: { // 第三方库 react: [‘react’, ‘react-dom’, ‘react-redux’] }, output: { // 输出的动态链接库的文件名称,[name] 代表当前动态链接库的名称, filename: ‘[name].dll.js’, path: resolve(‘dist/dll’), // library必须和后面dllplugin中的name一致 后面会说明 library: ‘[name]dll[hash]’ }, plugins: [ // 接入 DllPlugin new webpack.DllPlugin({ // 动态链接库的全局变量名称,需要和 output.library 中保持一致 // 该字段的值也就是输出的 manifest.json 文件 中 name 字段的值 name: ‘[name]dll[hash]’, // 描述动态链接库的 manifest.json 文件输出时的文件名称 path: path.join(__dirname, ‘dist/dll’, ‘[name].manifest.json’) }), ]}我们先来看看,这一步到底做了什么。执行:webpack –config webpack.dll.config,然后到指定的输出文件夹查看输出:react.dll文件里是使用数组保存的模块,索引值就作为id;react.manifest.json文件里,是用来描述对应的dll文件里保存的模块里暴露出刚刚构建的所有模块,如下:{ “name”:“react_dll_553e24e2c44987d2578f”, “content”:{ “./node_modules/webpack/node_modules/process/browser.js”:{“id”:0,“meta”:{}},"./node_modules/react/node_modules/fbjs/lib/invariant.js":{“id”:1,“meta”:{}},"./node_modules/react/lib/Object.assign.js":{“id”:2,“meta”:{}},"./node_modules/react/node_modules/fbjs/lib/warning.js":{“id”:3,“meta”:{}} //省略相似代码 }}2. 在主构建配置文件使用动态库文件在webpack.config中使用dll要用到DllReferencePlugin,这个插件通过引用 dll 的 manifest 文件来把依赖的名称映射到模块的 id 上,之后再在需要的时候通过内置的 webpack_require 函数来 require 他们. new webpack.DllReferencePlugin({ context: __dirname, manifest: require(’./dist/dll/react.manifest.json’) }),第一步产出的manifest文件就用在这里,给主构建流程作为查找dll的依据:DllReferencePlugin去 manifest.json 文件读取 name 字段的值,把值的内容作为在从全局变量中获取动态链接库中内容时的全局变量名,因此:在 webpack_dll.config.js 文件中,DllPlugin 中的 name 参数必须和 output.library 中保持一致。3. 在入口文件引入dll文件。生成的dll暴露出的是全局函数,因此还需要在入口文件里面引入对应的dll文件。<body> <div id=“app”></div> <!–引用dll文件–> <script src="../../dist/dll/react.dll.js" ></script></body>作用首先从前面的介绍,至少可以看出dll的两个作用分离代码,业务代码和第三方模块可以被打包到不同的文件里,这个有几个好处:避免打包出单个文件的大小太大,不利于调试将单个大文件拆成多个小文件之后,一定情况下有利于加载(不超出浏览器一次性请求的文件数情况下,并行下载肯定比串行快)提升构建速度。第三方库没有变更时,由于我们只构建业务相关代码,相比全部重新构建自然要快的多。注意事项从前面可以看到dll带来的优点,但并不意味着我们就应该把除业务代码外的所有代码全部都丢到dll中,举一个例子:1.对于lodash这种第三方库,正确的用法是只去import所需的函数(用什么引什么),例如:// 正确用法import isPlainObject from ’lodash/isPlainObject’//错误用法import { isPlainObject } from ’lodash’这两种写法的差别在于,打包时webpack会根据引用去打包依赖的内容,所以第一种写法,webpack只会打包lodash的isPlainObject库,第二种写法却会打包整个lodash。现在假设在项目中只是用到不同模块对lodash里的某几个函数并且没有对于某个函数重复使用非常多次,那么这时候把lodash添加到dll中,带来的收益就并不明显,反而导致2个问题:由于打包了整个lodash,而导致打包后的文件总大小(注意是总大小)比原先还要大在dll打包太多内容也需要耗费时间,虽然我们一般只在第三方模块更新之后才进行重新预编译(就是dll打包的过程),但是如果这个时间太长的话体验也不好、实践与反思放一张自己在一个比较大的项目中单纯使用dll之后的收益,提取的内容是 react相关的第三方库,和fish组件,构建时间从120s降低到80s左右(当然这个时间还是有点恐怖),构建前appjs的大小是680kb,拆分业务代码和第三方代码分别是400kb和380kb(这就是拆分后大小大于拆分前大小的例子),从这一点来看,对于常见第三方库是否要放进dll可能比较明确(比如react系列打包一般肯定不亏),但是还有一些就要结合具体的项目内容来进行判断和取舍。(强烈推荐使用webpack-bundle-analyzer插件进行性能分析)总结本文介绍了Dllplugin的思想,基本用法和应用场景(关于使用的部分更详细的内容可以看官方文档),结合个人的一些实践经验,对于常见第三方库是否要放进dll可能比较明确(比如react系列打包一般肯定不亏),但是还有一些就要结合具体的项目内容来判断,例如我上面的实践的例子就说明目前的拆分还不够好。这一块也欢迎大家一起探讨。如果内容有错误的地方欢迎指出(觉得看着不理解不舒服想吐槽也完全没问题);如果对你有帮助,欢迎点赞和收藏,转载请征得同意后著明出处,如果有问题也欢迎私信交流,主页有邮箱地址 ...

September 30, 2018 · 1 min · jiezi

webpack启动代码源码解读

前言虽然每天都在用webpack,但一直觉得隔着一层神秘的面纱,对它的工作原理一直似懂非懂。它是如何用原生JS实现模块间的依赖管理的呢?对于按需加载的模块,它是通过什么方式动态获取的?打包完成后那一堆/*****/开头的代码是用来干什么的?本文将围绕以上3个问题,对照着源码给出解答。如果你对webpack的配置调优感兴趣,可以看看我之前写的这篇文章:webpack调优总结模块管理先写一个简单的JS文件,看看webpack打包后会是什么样子:// main.jsconsole.log(‘Hello Dickens’);// webpack.config.jsconst path = require(‘path’);module.exports = { entry: ‘./main.js’, output: { filename: ‘bundle.js’, path: path.resolve(__dirname, ‘dist’) }};在当前目录下运行webpack,会在dist目录下面生成打包好的bundle.js文件。去掉不必要的干扰后,核心代码如下:// webpack启动代码(function (modules) { // 模块缓存对象 var installedModules = {}; // webpack实现的require函数 function webpack_require(moduleId) { // 检查缓存对象,看模块是否加载过 if (installedModules[moduleId]) { return installedModules[moduleId].exports; } // 创建一个新的模块缓存,再存入缓存对象 var module = installedModules[moduleId] = { i: moduleId, l: false, exports: {} }; // 执行模块代码 modules[moduleId].call(module.exports, module, module.exports, webpack_require); // 将模块标识为已加载 module.l = true; // 返回export的内容 return module.exports; } … // 加载入口模块 return webpack_require(webpack_require.s = 0);})([ / 0 / (function (module, exports) { console.log(‘Hello Dickens’); })]);代码是一个立即执行函数,参数modules是由各个模块组成的数组,本例子只有一个编号为0的模块,由一个函数包裹着,注入了module和exports2个变量(本例没用到)。核心代码是__webpack_require__这个函数,它的功能是根据传入的模块id,返回模块export的内容。模块id由webpack根据文件的依赖关系自动生成,是一个从0开始递增的数字,入口文件的id为0。所有的模块都会被webpack用一个函数包裹,按照顺序存入上面提到的数组实参当中。模块export的内容会被缓存在installedModules中。当获取模块内容的时候,如果已经加载过,则直接从缓存返回,否则根据id从modules形参中取出模块内容并执行,同时将结果保存到缓存对象当中(将在下文讲解)。我们再添加一个文件,在入口文件处导入,再来看看生成的启动文件是怎样的。// main.jsimport logger from ‘./logger’;console.log(‘Hello Dickens’);logger();//logger.jsexport default function log() { console.log(‘Log from logger’);}启动文件的模块数组:[ / 0 / (function (module, webpack_exports, webpack_require) { “use strict”; Object.defineProperty(webpack_exports, “__esModule”, { value: true }); / harmony import / var WEBPACK_IMPORTED_MODULE_0__logger = webpack_require(1); console.log(‘Hello Dickens’); Object(WEBPACK_IMPORTED_MODULE_0__logger[“a” / default / ])(); }), / 1 / (function (module, webpack_exports, webpack_require) { “use strict”; / harmony export (immutable) / webpack_exports[“a”] = log; function log() { console.log(‘Log from logger’); } })]可以看到现在有2个模块,每个模块的包裹函数都传入了module, webpack_exports, __webpack_require__三个参数,它们是通过上文提到的__webpack_require__注入的:// 执行模块代码modules[moduleId].call(module.exports, module, module.exports, webpack_require);执行的结果也保存在缓存对象中了。按需加载再对代码进行改造,来研究webpack是如何实现动态加载的:// main.jsconsole.log(‘Hello Dickens’);import(’./logger’).then(logger => { logger();});logger文件保持不变,编译后比之前多出了1个chunk。bundle_asy的内容如下:(function (modules) { // 加载成功后的JSONP回调函数 var parentJsonpFunction = window[“webpackJsonp”]; // 加载成功后的JSONP回调函数 window[“webpackJsonp”] = function webpackJsonpCallback(chunkIds, moreModules, executeModules) { var moduleId, chunkId, i = 0, resolves = [], result; for (; i < chunkIds.length; i++) { chunkId = chunkIds[i]; // installedChunks[chunkId]不为0且不为undefined,将其放入加载成功数组 if (installedChunks[chunkId]) { // promise的resolve resolves.push(installedChunks[chunkId][0]); } // 标记模块加载完成 installedChunks[chunkId] = 0; } // 将动态加载的模块添加到modules数组中,以供后续的require使用 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的对象,0表示已经加载成功 // 1是当前模块的编号,已加载完成 var installedChunks = { 1: 0 }; // require函数,跟上面的一样 function webpack_require(moduleId) { if (installedModules[moduleId]) { return installedModules[moduleId].exports; } var module = installedModules[moduleId] = { i: moduleId, l: false, exports: {} }; modules[moduleId].call(module.exports, module, module.exports, webpack_require); module.l = true; return module.exports; } // 按需加载,通过动态添加script标签实现 webpack_require.e = function requireEnsure(chunkId) { var installedChunkData = installedChunks[chunkId]; // chunk已经加载成功 if (installedChunkData === 0) { return new Promise(function (resolve) { resolve(); }); } // 加载中,返回之前创建的promise(数组下标为2) if (installedChunkData) { return installedChunkData[2]; } // 将promise相关函数保持到installedChunks中方便后续resolve或reject var promise = new Promise(function (resolve, reject) { installedChunkData = installedChunks[chunkId] = [resolve, reject]; }); installedChunkData[2] = promise; // 启动chunk的异步加载 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_async.js”; script.onerror = script.onload = onScriptComplete; var timeout = setTimeout(onScriptComplete, 120000); function onScriptComplete() { script.onerror = script.onload = null; clearTimeout(timeout); var chunk = installedChunks[chunkId]; // 正常的流程,模块加载完后会调用webpackJsonp方法,将chunk置为0 // 如果不为0,则可能是加载失败或者超时 if (chunk !== 0) { if (chunk) { // 调用promise的reject chunk[1](new Error(‘Loading chunk ’ + chunkId + ’ failed.’)); } installedChunks[chunkId] = undefined; } }; head.appendChild(script); return promise; }; … // 加载入口模块 return webpack_require(webpack_require.s = 0);})([ / 0 / (function (module, exports, webpack_require) { console.log(‘Hello Dickens’); // promise resolve后,会指定加载哪个模块 webpack_require.e / import() /(0) .then(webpack_require.bind(null, 1)) .then(logger => { logger(); }); })]);挂在到window下面的webpackJsonp函数是动态加载模块代码下载后的回调,它会通知webpack模块下载完成并将模块加入到modules当中。webpack_require.e函数是动态加载的核心实现,它通过动态创建一个script标签来实现代码的异步加载。加载开始前会创建一个promise存到installedChunks对象当中,加载成功则调用resolve,失败则调用reject。resolve后不会传入模块本身,而是通过__webpack_require__来加载模块内容,require的模块id由webpack来生成:webpack_require.e / import() /(0) .then(webpack_require.bind(null, 1)) .then(logger => { logger(); });接下来看下动态加载的chunk的代码,0.bundle_asy的内容如下:webpackJsonp([0], [ / 0 / , / 1 / (function (module, webpack_exports, webpack_require) { “use strict”; Object.defineProperty(webpack_exports, “__esModule”, { value: true }); / harmony export (immutable) */ webpack_exports[“default”] = log; function log() { console.log(‘Log from logger’); } })]);代码非常好理解,加载成功后立即调用上文提到的webpackJsonp方法,将chunkId和模块内容传入。这里要分清2个概念,一个是chunkId,一个moduleId。这个chunk的chunkId是0,里面只包含一个module,moduleId是1。一个chunk里面可以包含多个module。总结本文通过分析webpack生成的启动代码,讲解了webpack是如何实现模块管理和动态加载的,希望对你有所帮助。如果你对webpack的配置调优感兴趣,可以看看我之前写的这篇文章:webpack调优总结 ...

September 27, 2018 · 3 min · jiezi

手把手教会使用react开发日历组件

准备工作提前需要准备好react脚手架开发环境,由于react已经不支持在页面内部通过jsx.transform来转义,我们就自己大了个简易的开发环境创建一个文件夹,命名为react-canlendarcd ./react-canlendar运行npm init一路enter我们得到一个package.json的文件安装几个我们需要的脚手架依赖包npm install awesome-typescript-loader typescript webpack webpack-cli -D安装几个我们需要的类库npm install @types/react react react-dom –save基础类库安装完毕,开始构建webpack配置新建一个目录config,config下面新增一个文件,名字叫做webpack.jsvar path = require(‘path’)module.exports = { entry: { main: path.resolve(__dirname, ‘../src/index.tsx’) }, output: { filename: ‘[name].js’ }, resolve: { extensions: [".ts", “.tsx”, “.js”, “.json”] }, module: { rules: [ {test: /.tsx?$/, use: [‘awesome-typescript-loader’]} ] }}还需要创建一个index.html文件,这是我们的入口文件<!DOCTYPE html><html lang=“en”><head> <meta charset=“UTF-8”> <title>Document</title></head><body> <div id=“root”></div> <script src="./dist/main.js"></script></body></html>以上环境只是一个极简单的环境,真实环境要比这个复杂的多。好了,言归正传,我们还是聚焦到日历组件的开发中来吧创建一个src文件夹,内部创建一个index.tsx文件。这个入口文件很简单就是一个挂载import * as React from ‘react’import * as ReactDOM from ‘react-dom’ReactDOM.render(( <div> test </div>), document.getElementById(‘root’))ok,打开页面可以看到页面正常显示了test字样。我们需要创建Calendar组件了。创建一个components文件夹,内部创建一个Calendar.tsx文件。import * as React from ‘react’export default class Calendar extends React.Component { render() { return (<div> 日历 </div>) }}在index.tsx中把Calendar.tsx引入,并使用起来。于是index.tsx变成这个样子。import * as React from ‘react’import * as ReactDOM from ‘react-dom’import Calendar from ‘./components/Calendar’ReactDOM.render(( <div> <Calendar/> </div>), document.getElementById(‘root’))可以看到页面显示了日历字样。要显示日历,首先需要显示日历这个大框以及内部的一个个小框。实现这种布局最简单的布局就是table了所以我们首先创建的是这种日历table小框框,以及表头的星期排列。import * as React from ‘react’const WEEK_NAMES = [‘日’, ‘一’, ‘二’, ‘三’, ‘四’, ‘五’, ‘六’]const LINES = [1,2,3,4,5,6]export default class Calendar extends React.Component { render() { return (<div> <table cellPadding={0} cellSpacing={0} className=“table”> <thead> <tr> { WEEK_NAMES.map((week, key) => { return <td key={key}>{week}</td> }) } </tr> </thead> <tbody> { LINES.map((l, key) => { return <tr key={key}> { WEEK_NAMES.map((week, index) => { return <td key={index}>{index}</td> }) } </tr> }) } </tbody> </table> </div>) }}可以看到我们使用了一个星期数组作为表头,我们按照惯例是从周日开始的。你也可以从其他星期开始,不过会对下面的日期显示有影响,因为每个月的第一天是周几决定第一天显示在第几个格子里。那为什么行数要6行呢?因为我们是按照最大行数来确定表格的行数的,如果一个月有31天,而这个月的第一天刚好是周六。就肯定会显示6行了。为了显示好看,我直接写好了样式放置在index.html中了,这个不重要,不讲解。<!DOCTYPE html><html lang=“en”><head> <meta charset=“UTF-8”> <title>Document</title> <style type=“text/css”> * { margin: 0; padding: 0; } .table { border-collapse:collapse; border-spacing:0; } .table td{ border: 1px solid #ddd; padding: 10px; } .table caption .caption-header{ border-top: 1px solid #ddd; border-right: 1px solid #ddd; border-left: 1px solid #ddd; padding: 10px; display: flex; justify-content: space-between; } .table caption .caption-header .arrow { cursor: pointer; font-family: “宋体”; transition: all 0.3s; } .table caption .caption-header .arrow:hover { opacity:0.7; } </style></head><body> <div id=“root”></div> <script src="./dist/main.js"></script></body></html>下面就要开始显示日期了,首先要把当前月份的日期显示出来,我们先在组件的state中定义当前组件的状态state = { month: 0, year: 0, currentDate: new Date()}我们定义一个方法获取当前年月,为什么不需要获取日,因为日历都是按月显示的。获取日现在看来对我们没有意义,于是新增一个方法,设置当前组件的年月setCurrentYearMonth(date) { var month = Calendar.getMonth(date) var year = Calendar.getFullYear(date) this.setState({ month, year })}static getMonth(date: Date): number{ return date.getMonth()}static getFullYear(date: Date): number{ return date.getFullYear()}创建两个静态方法获取年月,为什么是静态方法,因为与组件的实例无关,最好放到静态方法上去。要想绘制一个月还需要知道一个月的天数吧,才好绘制吧所以我们创建一个数组来表示月份的天数const MONTH_DAYS = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] // 暂定2月份28天吧组件上创建一个函数,根据月份获取天数,也是静态的static getCurrentMonthDays(month: number): number { return MONTH_DAYS[month]}下面还有一个重要的事情,就是获取当前月份第一天是周几,这样子就可以决定把第一天绘制在哪里了。首先要根据年月的第一天获得date,根据这个date获取周几。static getDateByYearMonth(year: number, month: number, day: number=1): Date { var date = new Date() date.setFullYear(year) date.setMonth(month, day) return date }这里获得每个月的第一天是周几了。static getWeeksByFirstDay(year: number, month: number): number { var date = Calendar.getDateByYearMonth(year, month) return date.getDay() }好了,开始在框子插入日期数字了。因为每个日期都是不一样的,这个二维数组可以先计算好,或者通过函数直接插入到jsx中间。static getDayText(line: number, weekIndex: number, weekDay: number, monthDays: number): any { var number = line * 7 + weekIndex - weekDay + 1 if ( number <= 0 || number > monthDays ) { return <span>&nbsp;</span> } return number }看一下这个函数需要几个参数哈,第一个行数,第二个列数(周几),本月第一天是周几,本月天数。line * 7 + weekIndex表示当前格子本来是几,减去本月第一天星期数字。为什么+1,因为索引是从0开始的,而天数则是从1开始。那么<0 || >本月最大天数的则过滤掉,返回一个空span,只是为了撑开td。其他则直接返回数字。import * as React from ‘react’const WEEK_NAMES = [‘日’, ‘一’, ‘二’, ‘三’, ‘四’, ‘五’, ‘六’]const LINES = [1,2,3,4,5,6]const MONTH_DAYS = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]export default class Calendar extends React.Component { state = { month: 0, year: 0, currentDate: new Date() } componentWillMount() { this.setCurrentYearMonth(this.state.currentDate) } setCurrentYearMonth(date) { var month = Calendar.getMonth(date) var year = Calendar.getFullYear(date) this.setState({ month, year }) } static getMonth(date: Date): number{ return date.getMonth() } static getFullYear(date: Date): number{ return date.getFullYear() } static getCurrentMonthDays(month: number): number { return MONTH_DAYS[month] } static getWeeksByFirstDay(year: number, month: number): number { var date = Calendar.getDateByYearMonth(year, month) return date.getDay() } static getDayText(line: number, weekIndex: number, weekDay: number, monthDays: number): any { var number = line * 7 + weekIndex - weekDay + 1 if ( number <= 0 || number > monthDays ) { return <span>&nbsp;</span> } return number } static formatNumber(num: number): string { var _num = num + 1 return _num < 10 ? 0${_num} : ${_num} } static getDateByYearMonth(year: number, month: number, day: number=1): Date { var date = new Date() date.setFullYear(year) date.setMonth(month, day) return date } checkToday(line: number, weekIndex: number, weekDay: number, monthDays: number): Boolean { var { year, month } = this.state var day = Calendar.getDayText(line, weekIndex, weekDay, monthDays) var date = new Date() var todayYear = date.getFullYear() var todayMonth = date.getMonth() var todayDay = date.getDate() return year === todayYear && month === todayMonth && day === todayDay } monthChange(monthChanged: number) { var { month, year } = this.state var monthAfter = month + monthChanged var date = Calendar.getDateByYearMonth(year, monthAfter) this.setCurrentYearMonth(date) } render() { var { year, month } = this.state console.log(this.state) var monthDays = Calendar.getCurrentMonthDays(month) var weekDay = Calendar.getWeeksByFirstDay(year, month) return (<div> {this.state.month} <table cellPadding={0} cellSpacing={0} className=“table”> <caption> <div className=“caption-header”> <span className=“arrow” onClick={this.monthChange.bind(this, -1)}>&#60;</span> <span>{year} - {Calendar.formatNumber(month)}</span> <span className=“arrow” onClick={this.monthChange.bind(this, 1)}>&gt;</span> </div> </caption> <thead> <tr> { WEEK_NAMES.map((week, key) => { return <td key={key}>{week}</td> }) } </tr> </thead> <tbody> { LINES.map((l, key) => { return <tr key={key}> { WEEK_NAMES.map((week, index) => { return <td key={index} style={{color: this.checkToday(key, index, weekDay, monthDays) ? ‘red’ : ‘#000’}}> {Calendar.getDayText(key, index, weekDay, monthDays)} </td> }) } </tr> }) } </tbody> </table> </div>) }}可以看到最终的代码多了一些东西,因为我加了月份的切换。还记的上文我们把二月份天数写28天嘛?要不你们自己改改,判断一下闰年。 ...

September 7, 2018 · 4 min · jiezi

使用 TypeScript 改造构建工具及测试用例

最近的一段时间一直在搞TypeScript,一个巨硬出品、赋予JavaScript语言静态类型和编译的语言。 第一个完全使用TypeScript重构的纯Node.js项目已经上线并稳定运行了。 第二个前后端的项目目前也在重构中,关于前端基于webpack的TypeScript套路之前也有提到过:TypeScript在react项目中的实践。 但是这些做完以后也总感觉缺了点儿什么 (没有尽兴):是的,依然有五分之一的JavaScript代码存在于项目中,作为一个TypeScript的示例项目,表现的很不纯粹。 所以有没有可能将这些JavaScript代码也换成TypeScript呢? 答案肯定是有的,首先需要分析这些代码都是什么:Webpack打包时的配置文件一些简单的测试用例(使用的mocha和chai)知道了是哪些地方还在使用JavaScript,这件事儿就变得很好解决了,从构建工具(Webpack)开始,逐个击破,将这些全部替换为TypeScript。Webpack 的 TypeScript 实现版本在这8102年,很幸福,Webpack官方已经支持了TypeScript编写配置文件,文档地址。 除了TypeScript以外还支持JSX和CoffeeScript的解释器,在这就忽略它们的存在了依赖的安装首先是要安装TypeScript相关的一套各种依赖,包括解释器及该语言的核心模块:npm install -D typescript ts-nodetypescript为这个语言的核心模块,ts-node用于直接执行.ts文件,而不需要像tsc那样会编译输出.js文件。ts-node helloworld.ts因为要在TypeScript环境下使用Webpack相关的东东,所以要安装对应的types。 也就是Webpack所对应的那些*.d.ts,用来告诉TypeScript这是个什么对象,提供什么方法。npm i -D @types/webpack一些常用的pLugin都会有对应的@types文件,可以简单的通过npm info @types/XXX来检查是否存在 如果是一些小众的plugin,则可能需要自己创建对应的d.ts文件,例如我们一直在用的qiniu-webpack-plugin,这个就没有对应的@types包的,所以就自己创建一个空文件来告诉TypeScript这是个啥:declare module ‘qiniu-webpack-plugin’ // 就一个简单的定义即可// 如果还有其他的包,直接放到同一个文件就行了// 文件名也没有要求,保证是 d.ts 结尾即可放置的位置没有什么限制,随便丢,一般建议放到types文件夹下 最后就是.ts文件在执行时的一些配置文件设置。 用来执行Webpack的.ts文件对tsconfig.json有一些小小的要求。 compilerOptions下的target选项必须是es5,这个代表着输出的格式。 以及module要求选择commonjs。{ “compilerOptions”: { “module”: “commonjs”, “target”: “es5”, “esModuleInterop”: true }}但一般来讲,执行Webpack的同级目录都已经存在了tsconfig.json,用于实际的前端代码编译,很可能两个配置文件的参数并不一样。 如果因为要使用Webpack去修改真正的代码配置参数肯定是不可取的。 所以我们就会用到这么一个包,用来改变ts-node执行时所依赖的配置文件:tsconfig-paths 在Readme中发现了这样的说法:If process.env.TS_NODE_PROJECT is set it will be used to resolved tsconfig.json。 在Webpack的文档中同样也提到了这句,所以这是一个兼容的方法,在命令运行时指定一个路径,在不影响原有配置的情况下创建一个供Webpack打包时使用的配置。将上述的配置文件改名为其它名称,Webpack文档示例中为tsconfig-for-webpack-config.json,这里就直接沿用了然后添加npm script如下{ “scripts”: { “build”: “TS_NODE_PROJECT=tsconfig-for-webpack-config.json webpack –config configs.ts” }}文件的编写关于配置文件,从JavaScript切换到TypeScript实际上并不会有太大的改动,因为Webpack的配置文件大多都是写死的文本/常量。 很多类型都是自动生成的,基本可以不用手动指定,一个简单的示例:import { Configuration } from ‘webpack’const config: Configuration = { mode: process.env.NODE_ENV === ‘production’ ? ‘production’ : ‘development’,}export default configConfiguration是一个Webpack定义的接口(interface),用来规范一个对象的行为。 在VS Code下按住Command + 单击可以直接跳转到具体的webpack.d.ts定义文件那里,可以看到详细的定义信息。 各种常用的规则都写在了这里,使用TypeScript的一个好处就是,当要实现一个功能时你不再需要去网站上查询应该要配置什么,可以直接翻看d.ts的定义。 如果注释写得足够完善,基本可以当成文档来用了,而且在VS Code编辑器中还有动态的提示,以及一些错误的纠正,比如上述的NODE_ENV的获取,如果直接写process.env.NODE_ENV || ‘development’是会抛出一个异常的,因为从d.ts中可以看到,关于mode只有三个有效值production、developemnt和none,而process.env.NODE_ENV显然只是一个字符串类型的变量。 所以我们需要使用三元运算符保证传入的参数一定是我们想要的。 以及在编写的过程中,如果有一些自定义的plugin之类的,可能在使用的过程中会抛异常提示说某个对象不是有效的Plugin对象,一个很简单的方法,在对应的plugin后边添加一个as webpack.Plugin即可。 在这里TypeScript所做的只是静态的检查,并不会对实际的代码执行造成任何影响,就算类型因为强行as而改变,也只是编译期的修改,在实际执行的JavaScript代码中还是弱类型的 在完成了上述的操作后,再执行npm run XXX就可以直接运行TypeScript版本的Webpack配置咯。探索期间的一件趣事因为我的项目根目录已经安装了ts-node,而前端项目是作为其中的一个文件夹存在的,所以就没有再次进行安装。 这就带来了一个令人吐血的问题。 首先全部流程走完以后,我直接在命令行中输入TS_NODE_PROJECT=XXX.json NODE_ENV=dev webpack –config ./webpack/dev.ts 完美运行,然后将这行命令放到了npm scripts中:{ “scripts”: { “start”: “TS_NODE_PROJECT=XXX.json NODE_ENV=dev webpack –config ./webpack/dev.ts” }}再次运行npm start,发现竟然出错了-.-,提示我说import语法不能被识别,这个很显然就是没有应用我们在ts_NODE_PROJECT中指定的config文件。 刚开始并不知道问题出在哪,因为这个在命令行中直接执行并没有任何问题。 期间曾经怀疑是否是环境变量没有被正确设置,还使用了cross-env这个插件,甚至将命令写到了一个sh文件中进行执行。 然而问题依然存在,后来在一个群中跟小伙伴们聊起了这个问题,有人提出,你是不是全局安装了ts-node。 检查以后发现,果然是的,在命令行执行时使用的是全局的ts-node,但是在npm scripts中使用的是本地的ts-node。 在命令行环境执行时还以为是会自动寻找父文件夹node_modules下边的依赖,其实是使用的全局包。 乖乖的在client-src文件夹下也安装了ts-node就解决了这个问题。 全局依赖害人。。测试用例的改造前边的Webpack改为TypeScript大多数原因是因为强迫症所致。 但是测试用例的TypeScript改造则是一个能极大提高效率的操作。为什么要在测试用例中使用 TypeScript测试用例使用chai来编写,(之前的Postman也是用的chai的语法) chai提供了一系列的语义化链式调用来实现断言。 在之前的分享中也提到过,这么多的命令你并不需要完全记住,只知道一个expect(XXX).to.equal(true)就够了。 但是这样的通篇to.equal(true)是巨丑无比的,而如果使用那些语义化的链式调用,在不熟练的情况下很容易就会得到:Error: XXX.XXX is not a function因为这确实有一个门槛问题,必须要写很多才能记住调用规则,各种not、includes的操作。 但是接入了TypeScript以后,这些问题都迎刃而解了。 也是前边提到的,所有的TypeScript模块都有其对应的.d.ts文件,用来告诉我们这个模块是做什么的,提供了什么可以使用。 也就是说在测试用例编写时,我们可以通过动态提示来快速的书写断言,而不需要结合着文档去进行“翻译”。 使用方式如果是之前有写过mocha和chai的童鞋,基本上修改文件后缀+安装对应的@types即可。 可以直接跳到这里来:开始编写测试脚本 但是如果对测试用例感兴趣,但是并没有使用过的童鞋,可以看下边的一个基本步骤。安装依赖TypeScript相关的安装,npm i -D typescript ts-nodeMocha、chai相关的安装,npm i -D mocha chai @types/mocha @types/chai如果需要涉及到一些API的请求,可以额外安装chai-http,npm i -D chai-http @types/chai-http环境的依赖就已经完成了,如果额外的使用一些其他的插件,记得安装对应的@types文件即可。 如果有使用ESLint之类的插件,可能会提示modules必须存在于dependencies而非devDependencies 这是ESLint的import/no-extraneous-dependencies规则导致的,针对这个,我们目前的方案是添加一些例外:import/no-extraneous-dependencies: - 2 - devDependencies: - “/*.test.js” - “/.spec.js” - “**/webpack” - “/webpack/*“针对这些目录下的文件/文件夹不进行校验。_是的,webpack的使用也会遇到这个问题_开始编写测试脚本如果是对原有的测试脚本进行修改,无外乎修改后缀、添加一些必要的类型声明,不会对逻辑造成任何修改。一个简单的示例// number-comma.tsexport default (num: number | string) => String(num).replace(/B(?=(d{3})+$)/g, ‘,’)// number-comma.spec.tsimport chai from ‘chai’import numberComma from ‘./number-comma’const { expect } = chai// 测试项describe(’number-comma’, () => { // 子项目1 it(’1234567 should transform to 1,234,567’, done => { expect(numberComma(1234567)).to.equal(‘1,234,567’) done() }) // 子项目2 it(’123 should never transform’, done => { const num = 123 expect(numberComma(num)).to.equal(String(num)) done() })})如果全局没有安装mocha,记得将命令写到npm script中,或者通过下述方式执行./node_modules/mocha/bin/mocha -r ts-node/register test/number-comma.spec.ts# 如果直接这样写,会抛出异常提示 mocha 不是命令mocha -r ts-node/register test/number-comma.spec.tsmocha有一点儿比较好的是提供了-r命令来让你手动指定执行测试用例脚本所使用的解释器,这里直接设置为ts-node的路径ts-node/register,然后就可以在后边直接跟一个文件名(或者是一些通配符)。 目前我们在项目中批量执行测试用例的命令如下:{ “scripts”: { “test”: “mocha -r ts-node/register test//*.spec.ts” }}npm test可以直接调用,而不需要添加run命令符,类似的还有start、build等等 一键执行以后就可以得到我们想要的结果了,再也不用担心一些代码的改动会影响到其他模块的逻辑了 (前提是认真写测试用例) 小结做完上边两步的操作以后,我们的项目就实现了100%的TypeScript化,在任何地方享受静态编译语法所带来的好处。 附上更新后的代码含量截图:最近针对TypeScript做了很多事情,从Node.js、React以及这次的Webpack与Mocha+Chai。 TypeScript因为其存在一个编译的过程,极大的降低了代码出bug的可能性,提高程序的稳定度。 全面切换到TypeScript更是能够降低在两种语法之间互相切换时所带来的不必要的消耗,祝大家搬砖愉快。之前关于 TypeScript 的笔记TypeScript在node项目中的实践TypeScript在react项目中的实践一个完整的 TypeScript 示例typescript-example欢迎各位来讨论关于TypeScript使用上的一些问题,针对稳重的感觉不足之处也欢迎指出。参考资料ts-nodeconfiguration-languages | webpackmochajschaijs ...

September 3, 2018 · 2 min · jiezi

webpack4.17.1起步

本地安装webpacknpm install –save-dev webpack如果使用webpack4+版本,需要安装webpack-clinpm install webpack webpack-cli –save-dev初始化npm初始化npm后,会生根目录下成一个package.jsonnpm init -y创建文件目录,文件,内容文件结构webpack|-/src |-index.js|-index.html|-package.jsonsrc/index.jsfunction component () { var ele = document.createElement(‘div’) // Lodash[Lodash 通过降低 array、number、objects、string 等等的使用难度从而让 JavaScript 变得更简单。 // Lodash 的模块化方法 非常适用于: // 遍历 array、object 和 string // 对值进行操作和检测创建符合功能的函数 https://www.lodashjs.com/](目前通过一个 script 脚本引入)对于执行这一行是必需的 element.innerHTML = _.join([‘Hello’, ‘webpack’], ’ ‘); return element;}document.body.appendChild(component());index.html<!doctype html><html> <head> <title>起步</title> <script src=“https://unpkg.com/lodash@4.16.6"></script> </head> <body> <script src=”./src/index.js"></script> </body></html>package.json实际情况中,json文件中不允许有注释,如果有注释,会导致安装不了依赖{ … “name”: “webpack”, “version”: “1.0.0”, // “main”: “index.js”, // 并且移除 main 入口 “private”: true, // 以便确保我们安装包是私有的(private) 这可以防止意外发布你的代码 …}总结:通过script脚本引入三方资源会带来一些问题,如下:无法立即体现,脚本的执行依赖于外部扩展库(external library)如果依赖不存在,或者引入顺序错误,应用程序将无法正常运行如果依赖被引入但是并没有使用,浏览器将被迫下载无用代码使用 webpack 来管理这些脚本创建一个 bundle 文件调整目录结构webpack|-/src “源"代码,用于书写和编辑的代码 |-index.js|-/dist “分发"代码是构建过程产生的代码最小化和优化后的“输出”目录,最终将在浏览器中加载 |-index.html|-package.json不再使用script引入三方资源,要在 index.js 中打包 lodash 依赖,安装lodashnpm install –save-dev lodash修改src/index.js文件// 不再使用script引入lodash 本地已安装loadash,使用import引入lodashimport _ from ’lodash’function component () { var ele = document.createElement(‘div’) element.innerHTML = _.join([‘Hello’, ‘webpack’], ’ ‘); return element;}document.body.appendChild(component());修改dist/index.html文件<!doctype html><html> <head> <title>起步</title> <!– 因为现在是通过 import 引入 lodash,所以将 lodash <script> 删除 –> <!– <script src=“https://unpkg.com/lodash@4.16.6"></script> –> </head> <body> <!– 然后修改另一个 <script> 标签来加载 bundle,而不是原始的 /src 文件–> <script src=“main.js”></script> <!– <script src=”./src/index.js”></script> –> </body></html>执行npx webpack,构建成功后,在浏览器中打开index.html使用一个配置文件新增webpack.config.jsconst path = require(‘path’)module.exports = { entry: ‘./src/index.js’, // 入口文件 output: { // 构建后的bundle.js文件输出到dist文件中 filename: ‘bundle.js’, // _dirname表示当前文件所在的目录的绝对路径 path: path.resolve(_dirname, ‘dist’) }};再次npx webpack –config webpack.config.js使用cli(npx webpack)来运行本地node_module/.bin/webpack文件,反正我是一直没有构建成功,可能是本地环境问题。QAQ ===使用npm脚本太好了,还好还有另外一种方式,555,我们可以设置一个快捷方式。在 package.json 添加一个 npm 脚本(npm script)package.json 【package.json文件中不能有注释】{ … “scripts”: { “test”: “echo “Error: no test specified”” && exit 1"” ...

August 30, 2018 · 1 min · jiezi