开发脚手架与自动化构建工作流封装

去年6月24号开始工作,到今天刚好一周年了,纪念一下,分享最近学习的前端工程化笔记。

一、前端工程化

前端工程化是指遵循一定的标准和规范,通过工具去提高效率、降低成本的一种手段。

1. 前端开发中遇到的问题

  • 想要使用ES6+新特性,但是兼容有问题
  • 想要使用Less/Sass/PostCSS增强CSS的编程性,但是运行环境不能直接支持
  • 想要使用模块化的方式提高项目的可维护性,但运行环境不能直接支持
  • 部署上线前需要手动压缩代码及资源文件,部署过程需要手动上传代码到服务器
  • 多人协同开发,无法硬性统一大家的代码风格,从仓库中pull回来的代码质量无法保证

2. 主要解决的问题

  • 传统语言或语法的弊端
  • 无法使用模块化/组件化
  • 重复的机械式工作
  • 代码风格统一、质量保证
  • 依赖后端服务接口支持
  • 整体依赖后端项目

3. 工程化表现

  • 创建项目

    • 创建项目结构
    • 创建特定类型文件
  • 编码
    • 格式化代码
    • 校验代码风格
    • 编译/构建/打包
  • 预览/测试
    + Web Server / Mock

    • Live Reloading / HMR
    • Source Map
  • 提交
    • Git Hooks
    • Lint-staged
    • 持续集成
  • 部署
    • CI / CD
    • 自动发布

4. 工程化不等于某个具体工具

工具并不是工程化的核心,工程化的核心是对项目的整体规划或架构,工具只是落地和实现工程化的一个手段

一些成熟的工程化集成:

  • create-react-app
  • vue-cli
  • angular-cli
  • gatsby-cli

上面的几个是某个项目的官方提供的集成化方案

5. 工程化与Node.js

工程化工具都是Node.js开发的

二、脚手架工具

脚手架的本质作用就是创建项目基础结构、提供项目规范和约定。

1. 脚手架工具的作用

因为在前端工程中,可能会有:

  • 相同的组织结构
  • 相同的开发范式
  • 相同的模块依赖
  • 相同的工具配置
  • 相同的基础代码

脚手架就是解决上面问题的工具,通过创建项目骨架自动的执行工作。IDE创建项目的过程就是一个脚手架的工作流程。

由于前端技术选型比较多样,又没有一个统一的标准,所以前端脚手架不会集成在某一个IDE中,一般都是以一个独立的工具存在,相对会复杂一些。

2. 常用的脚手架工具

  • 第一类脚手架是根据信息创建对应的项目基础结构,适用于自身所服务的框架的那个项目。

    • create-react-app
    • vue-cli
    • Angular-cli
  • 第二类是像Yeoman为代表的通用型脚手架工具,会根据模板生成通用的项目结构,这种脚手架工具很灵活,很容易扩展。

  • 第三类以Plop为代表的脚手架工具,是在项目开发过程中,创建一些特定类型的组件,例如创建一个组件/模块所需要的文件,这些文件一般都是由特定结构组成的,有相同的结构。

3. 通用脚手架工具剖析

(1)Yeoman + Generator

Yeoman是最老牌、最强大、最通用的脚手架工具,是创建现代化应用的脚手架工具,不同于vue-cli,Yeoman更像是脚手架运行平台,我们可以通过Yeoman搭配不同的Generator去创建任何类型的项目,我们可以创建我们自己的Generator,从而去创建我们自己的前端脚手架。缺点是,在框架开发的项目中,Yeoman过于通用不够专注。

如果使用Yeoman:

  • 在电脑上全局安装Yeoman:yarn global add yo
  • Yeoman要搭配相应的Generator创建任务,所以要安装Generator。例如是创建node项目,则安装generator-node:yarn global add generator-node
  • 创建一个空文件夹:mkdir my-module, 然后进入文件夹:cd my-module
  • 通过Yeoman的yo命令安装刚才的生成器(去掉生成器名字前的generator-):yo node
  • 交互模式填写一些项目信息,会生成项目基础结构,并且生成一些项目文件,然后自动运行npm install安装一些项目依赖。

(2)SubGenerator

有时候我们可能不需要创建一个完成的项目结构,而是在已有项目的基础上,创建一些项目文件,如README.md,或者是创建一些特定类型的文件,如ESLint、Babel配置文件

  • 运行SubGenerator的方式就是在原有Generator基础上加上:SubGenerator的名字,如:yo node:cli
  • 在使用SubGenerator前,要先去查看一下Generator之下有哪些SubGenerator

(3)Plop

Plop是一个小而美的脚手架工具,通常用于创建项目中特定类型文件的小工具,一般是把Plop集成到项目中,用来自动化创建同类型的项目文件。

如何使用Plop创建文件:

  • 将plop模块作为项目开发依赖安装
  • 在项目根目录下创建一个plopfile.js文件
  • 在plopfile.js文件中定义脚手架任务
  • 编写用于生成特定类型文件的模板
  • 通过Plop提供的cli运行脚手架任务

4. 脚手架工作原理

脚手架的工作原理就是在启动脚手架之后,回自动地去询问一些预设问题,通过回答的结果结合一些模板文件,生成项目的结构。

使用NodeJS开发一个小型的脚手架工具:

  • yarn init初始化一个空文件夹:sample-scaffolding

  • package.json中添加bin属性指定脚手架的命令入口文件为cli.js

{"name": "sample-scaffolding","version": "1.0.0","main": "index.js","bin": "cli.js","license": "MIT","dependencies": {"ejs": "^3.1.3","inquirer": "^7.1.0"}
}
  • 编写cli.js
#!/usr/bin/env node// Node CLI 应用入口文件必须要有这样的文件头
// 如果Linux 或者 Mac 系统下,还需要修改此文件权限为755: chmod 755 cli.js// 脚手架工作过程:
// 1. 通过命令行交互询问用户问题
// 2. 根据用户回答的结果生成文件const path = require('path')
const fs = require('fs')
const inquirer = require('inquirer') // 发起命令行交互询问
const ejs = require('ejs') // 模板引擎
inquirer.prompt([{type: 'input',name: 'name',message: 'Project name?'}
]).then(answer => {console.log(answer)// 模板目录const tempDir = path.join(__dirname, 'templates')// 目标目录const destDir = process.cwd()// 将模板下的文件全部转换到目标目录fs.readdir(tempDir, (err, files) => {if (err) throw errfiles.forEach(file => {// 通过模板引擎渲染文件ejs.renderFile(path.join(tempDir, file), answer, (err, result) => {if(err) throw err// 将结果写入到目标目录fs.writeFileSync(path.join(destDir, file), result)})})})
})
  • 将该cli程序link到全局:yarn link
  • 然后再其他文件夹中执行:sample-scaffolding命令,就可以根据模板自动化创建文件了。

5. 自定义Generator开发脚手架

|-- generators/ ······生成器目录

| |-- app/ ······默认生成器目录

| |–templates ······模板文件夹

| |–foo.txx ······模板文件

| |–index.js ······默认生成器实现

| |–component/ ······其他生成器目录

| |–index.js ······其他生成器实现

|–package.json ······模块包配置文件

注意:Yeoman的生成器名称必须是generator-<name>,安装生成器的时候,就执行yo <name>

创建Generator生成器的步骤:

  • mkdir generator-sample

  • cd generator-sample

  • yarn init

  • yarn add yeoman-generator

  • 创建文件:generators/app/index.jsx

    // 此文件作为Generator的核心入口
    // 需要导出一个集成字Yeoman Generator的类型
    // Yeoman Generator在工作时会自动调用我们在此类型中定义的一些生命周期方法
    // 我们在这些方法中可以通过调用父类提供的一些工具方法实现一些功能,比如文件写入const Generator = require('yeoman-generator')module.exports = class extends Generator {prompting () {// Yeoman 在询问用户环节会自动调用次方法// 在此方法中可以调用父类的prompt()方法发出对用户命令行询问return this.prompt([{type: 'input',name: 'name',message: 'Your project name',default: this.appname // appname为项目生成目录}]).then( answers => {// answers => {name: 'user input value'}this.answers = answers})}writing () {// Yeoman 自动在生成文件阶段调用次方法// 我们这里尝试往项目目录中写入文件// this.fs.write(//   this.destinationPath('temp.txt'),//   Math.random().toString()// )// 通过模板方法导入文件到目标目录// 模板文件路径const tmpl = this.templatePath('foo.txt')// 输出目标路径const output = this.destinationPath('foo.txt')// 模板数组上下文const context = {title: 'Hello', success: false}// const context = this.answers // 从命令行获取的参数this.fs.copyTpl(tmpl, output, context)}
    }
    
  • templates/foo.txt作为模板文件

    这是一个模板文件
    内部可以使用EJS模板标记输出数据例如:<%= title %>其他的EJS语法也支持<%if (success) {%>hello world
    <%}%>
    
  • 执行yarn link, 此时这个模块就会作为全局模块被link到全局,别的项目可以直接使用它。

  • 创建一个别的文件夹my-proj, 在这个文件夹中执行:yo sample

  • 发布到npmjs网站上:yarn publish --registry=https://registry.yarnpkg.com

5. Plop

yarn add plop
plopfile.js

// Plop 入口文件,需要导入一个函数
// 此函数接受一个plop对象,用户创建生成器任务module.exports = plop => {plop.setGenerator('component', {description: 'create a component',prompts: [{type: 'input',name: 'name',message: 'component name',default: 'MyComponent'}],actions: [{type: 'add', // 代表添加文件path: 'src/components/{{name}}/{{name}}.js',templateFile: 'plop-templates/component.hbs'},{type: 'add', // 代表添加文件path: 'src/components/{{name}}/{{name}}.css',templateFile: 'plop-templates/component.css.hbs'},]})
}

编写模板:

component.hbs:

import React from 'react';export default () => (<div className="{{name}}"><h1>{{name}} Component</h1></div>
)

Component.css.hbs:

import React from 'react';
import ReactDOM from 'react-dom';
import {{name}} from './{{name}}';it('renders without crashing', () => {const div = documents.createElement('div');ReactDOM.render(<{{name}}/>, div);ReactDOM.unmountComponentAtNode(div)
})

执行命令:yarn plop component

三、自动化构建

源代码自动化构建成生产代码,也称为自动化构建工作流。

使用提高效率的语法、规范和标准,如:ECMAScript Next、Sass、模板引擎,这些用法大都不被浏览器直接支持,自动化工具就是解决这些问题的,构建转换那些不被支持的特性。

1. NPM Scripts

在package.json中增加一个scripts对象,如:

{"scripts": {"build": "sass scss/main.scss css/style.css"}
}

scripts可以自动发现node_modules里面的命令,所以不需要写完整的路径,直接写命令的名称就可以。然后可以通过npm或yarn运行scripts下面的命令名称,npm用run启动,yarn可以省略run,如:npm run buildyarn build

NPM Scripts是实现自动化构建工作流的最简方式。

{"scripts": {"build": "sass scss/main.scss css/style.css","preserve": "yarn build","serve": "browser-sync ."}
}

preserve是一个钩子,保证在执行serve之前,会先执行build,使样式先处理,然后再执行serve。

通过--watch可以监听sass文件的变化自动编译,但是此时sass命令在工作时,命令行会阻塞,去等待文件的变化,导致了后面的serve无法去工作,此时就需要同时去执行多个任务,要安装npm-run-all这个模块

{"scripts": {"build": "sass scss/main.scss css/style.css --watch","serve": "browser-sync .","start": "run-p build serve"}
}

运行npm run start命令,build和serve就会被同时执行。

2. Grunt

Grunt是最早的前端构建系统,它的插件生态非常完善,它的插件可以帮你完成任何你想做的事情。由于Grunt工作过程是基于临时文件去实现的,所以会比较慢。

如何使用Grunt:

  • 安装grunt:yarn add grunt ,编写gruntfile.js文件,下面举例grunt任务的几种用法:
// Grunt的入口文件
// 用于定义一些需要Grunt自动执行的任务
// 需要导出一个函数
// 此函数接受一个grunt的形参,内部提供一些创建任务时可以用到的APImodule.exports = grunt => {grunt.registerTask('foo', () => {// 第一个参数是任务名字,第二个参数接受一个回调函数,是指定任务的执行内容,执行命令是yarn grunt fooconsole.log('hello grunt ~')})grunt.registerTask('bar', '任务描述', () => { // 如果第二个参数是字符串,则是任务描述,执行命令是yarn grunt barconsole.log('other task~')})grunt.registerTask('default', () => { // 如果任务名称是'default',则为默认任务,grunt在运行时不需要执行任务名称,自动执行默认任务,执行命令是yarn gruntconsole.log('default task')})grunt.registerTask('default', ['foo', 'bad', 'bar']) // 一般用default映射其他任务,第二个参数传入一个数组,数组中指定任务的名字,grunt执行默认任务,则会依次执行数组中的任务,执行命令是yarn grunt// grunt.registerTask('async-task', () => {//   setTimeout(() => {//     console.log('async task working')//   }, 1000);// })// 异步任务,done()表示结束grunt.registerTask('async-task', function () { // grunt代码默认支持同步模式,如果需要异步操作,则需要通过this.async()得到一个回调函数,在你的异步操作完成过后,去调用这个回调函数,标记这个任务已经被完成。知道done()被执行,grunt才会结束这个任务的执行。执行命令是yarn grunt async-taskconst done = this.async()setTimeout(() => {console.log('async task working..')done()}, 1000);})// 失败任务grunt.registerTask('bad', () => { // 通过return false标志这个任务执行失败,执行命令是yarn grunt bad。如果是在任务列表中,这个任务的失败会导致后序所有任务不再被执行,执行命令是yarn grunt。可以通过--force参数强制执行所有的任务,,执行命令是yarn grunt default --forceconsole.log('bad working...')return false})// 异步失败任务,done(false)表示任务失败,执行命令是yarn grunt bad-async-taskgrunt.registerTask('bad-async-task', function () {const done = this.async()setTimeout(() => {console.log('bad async task working..')done(false)}, 1000);})
}
  • grunt配置选项

    module.exports = grunt => {grunt.initConfig({// 对象的属性名一般与任务名保持一致。// foo: 'bar'foo: {bar: 123}})grunt.registerTask('foo', () => {// console.log(grunt.config('foo')) // barconsole.log(grunt.config('foo.bar')) // 123.grunt的config支持通过foo.bar的形式获取属性值,也可以通过获取foo对象,然后取属性})
    }
    
  • 多目标任务(相当于子任务)

    module.exports = grunt => {grunt.initConfig({// 与任务名称同名build: {options: { // 是配置选项,不会作为任务foo: 'bar'},// 每一个对象属性都是一个任务css: {options: { // 会覆盖上层的optionsfoo: 'baz'}},// 每一个对象属性都是一个任务js: '2'}})// 多目标任务,可以让任务根据配置形成多个子任务,registerMultiTask方法,第一个参数是任务名,第二个参数是任务的回调函数grunt.registerMultiTask('build', function () {console.log(this.options())console.log(`build task: ${this.target}, data: ${this.data}`)})
    }
    

    执行命令:yarn grunt build, 输出结果:

    Running "build:css" (build) task{ foo: 'baz' }build task: css, data: [object Object]Running "build:js" (build) task{ foo: 'bar' }build task: js, data: 2
    
  • grunt插件使用

    插件机制是grunt的核心,因为很多构建任务都是通用的,社区当中也就出现了很多通用的插件,这些插件中封装了很多通用的任务,一般情况下我们的构建过程都是由通用的构建任务组成的。先去npm中安装 需要的插件,再去gruntfile中使用grunt.loadNpmTasks方法载入这个插件,最后根据插件的文档完成相关的配置选项。

    例如使用clean插件,安装 yarn add grunt-contrib-clean,用来清除临时文件。

    module.exports = grunt => {// 多目标任务需要通过initConfig配置目标grunt.initConfig({clean: {temp: 'temp/**' // ** 表示temp下的子目录以及子目录下的文件}})grunt.loadNpmTasks('grunt-contrib-clean')
    }
    

    执行:yarn grunt clean ,就会删除temp文件夹

  • Grunt常用插件总结:

    • grunt-sass
    • grunt-babel
    • grunt-watch
    const sass = require('sass')
    const loadGruntTasks = require('load-grunt-tasks')
    module.exports = grunt => {grunt.initConfig({sass: {options: {sourceMap: true,implementation: sass, // implementation指定在grunt-sass中使用哪个模块对sass进行编译,我们使用npm中的sass},main: {files: {'dist/css/main.css': 'src/scss/main.scss'}}},babel: {options: {presets: ['@babel/preset-env'],sourceMap: true},main: {files: {'dist/js/app.js': 'src/js/app.js'}}},watch: {js: {files: ['src/js/*.js'],tasks: ['babel']},css: {files: ['src/scss/*.scss'],tasks: ['sass']}}})// grunt.loadNpmTasks('grunt-sass')loadGruntTasks(grunt) // 自动加载所有的grunt插件中的任务grunt.registerTask('default', ['sass', 'babel', 'watch'])
    }
    

3. Gulp

Gulp是目前世界上最流行的前端构建系统,其核心特点就是高效、易用。它很好的解决了Grunt中读写磁盘慢的问题,Gulp是基于内存操作的。Gulp支持同时执行多个任务,效率自然大大提高,而且它的使用方式相对于Grunt更加易懂,而且Gulp的生态也非常完善,所以后来居上,更受欢迎。

  • gulp的使用

安装gulp:yarn add gulp,然后编写gulpfile.js,通过导出函数成员的方式定义gulp任务

// gulp的入口文件
exports.foo = done => {console.log('foo task working...')done() // 使用done()标识任务完成
}exports.default = done => {console.log('default task working...')done()
}

执行命令:yarn gulp foo执行foo任务, 或者yarn gulp执行默认任务default

gulp4.0之前的任务写法:

const gulp = require('gulp')gulp.task('bar', done => {console.log('bar working...')done()
})

执行命令yarn gulp bar可以运行bar任务,gulp4.0之后也保留了这个API,但是不推荐使用了

  • gulp创建组合任务:series串行、parallel并行

    const {series, parallel} = require('gulp')// gulp的入口文件
    exports.foo = done => {console.log('foo task working...')done() // 标识任务完成
    }exports.default = done => {console.log('default task working...')done()
    }const task1 = done => {setTimeout(() => {console.log('task1 working...')done()}, 1000);
    }const task2 = done => {setTimeout(() => {console.log('task2 working...')done()}, 1000);
    }const task3 = done => {setTimeout(() => {console.log('task3 working...')done()}, 1000);
    }// series 串行执行
    // exports.bar = series(task1, task2, task3)// parallel 并行执行
    exports.bar = parallel(task1, task2, task3)
    
  • Gulp的异步任务:

    const fs = require('fs')exports.callback = done => {console.log('callback task...')done() // 通过使用done()标志异步任务执行结束
    }exports.callback_error = done => {console.log('callback task...')done(new Error('task failed!')) // done函数也是错误优先回调函数。如果这个任务失败了,后序任务也不会工作了
    }exports.promise = () => {console.log('promise task...')return Promise.resolve() // resolve执行的时候,表示异步任务执行结束了。resolve不需要参数,因为gulp会忽略它的参数
    }exports.promise_error = () => {console.log('promise task...')return Promise.reject(new Error('task failed')) // reject标志这是一个失败的任务,后序的任务也会不再执行
    }const timeout = time => {return new Promise(resolve => {setTimeout(resolve, time);})
    }
    exports.async = async() => {await timeout(1000) // 在node8以上可以使用async和await,await的就是一个Promise对象console.log('async task...')
    }exports.stream = (done) => { // 最常用的就是基于stream的异步任务const readStream = fs.createReadStream('package.json')const writeSteam = fs.createWriteStream('temp.txt')readStream.pipe(writeSteam)return readStream  // 相当于下面的写法// readStream.on('end', () => {//    done()// })
    }
    
  • Gulp构建过程,例子:压缩CSS

    const fs = require('fs')
    const {Transform} = require('stream')exports.default = () => {const read = fs.createReadStream('normalize.css')const write = fs.createWriteStream('normalize.min.css')// 文件转化流const transform = new Transform({transform: (chunk, encoding, callback) => {// 核心转化过程// chunk => 读取流中读取的内容(Buffer )const input = chunk.toString()// 转化空白符和注释const output = input.replace(/\s+/g, '').replace(/\/\*.+?\*\//g, '')callback(null, output)}})read.pipe(transform) // 先转化.pipe(write)return read
    }
    
  • Gulp文件api

    const {src, dest} = require('gulp')
    const cleanCss = require('gulp-clean-css')
    const rename = require('gulp-rename')exports.default = () => {return src('src/*.css').pipe(cleanCss()).pipe(rename({ extname: '.min.css' })).pipe(dest('dist'))
    }
    
  • Gulp构建

    // 实现这个项目的构建任务
    const {src, dest, parallel, series, watch} = require('gulp')const del = require('del')
    const browserSync = require('browser-sync')const bs = browserSync.create()const loadPlugins = require('gulp-load-plugins')
    const plugins = loadPlugins()const {sass, babel, swig, imagemin} = pluginsconst data = {menus: [{name: 'Home',icon: 'aperture',link: 'index.html'},{name: 'Features',link: 'features.html'},{name: 'About',link: 'about.html'},{name: 'Contact',link: '#',children: [{name: 'Twitter',link: 'https://twitter.com/w_zce'},{name: 'About',link: 'https://weibo.com/zceme'},{name: 'divider'},{name: 'About',link: 'https://github.com/zce'}]}],pkg: require('./package.json'),date: new Date()
    }const clean = () => {return del(['dist', 'temp'])
    }const style = () => {return src('src/assets/styles/*.scss', { base: 'src' }).pipe(sass({ outputStyle: 'expanded' })).pipe(dest('temp')).pipe(bs.reload({stream: true}))
    }const script = () => {return src('src/assets/scripts/*.js', { base: 'src' }).pipe(babel({ presets: ['@babel/preset-env'] })).pipe(dest('temp')).pipe(bs.reload({stream: true}))
    }const page = () => {return src('src/**/*.html', {base: 'src'}).pipe(swig(data)).pipe(dest('temp')).pipe(bs.reload({stream: true}))
    }const image = () => {return src('src/assets/images/**', {base: 'src'}).pipe(imagemin()).pipe(dest('dist'))
    }const font = () => {return src('src/assets/fonts/**', {base: 'src'}).pipe(imagemin()).pipe(dest('dist'))
    }const extra = () => {return src('public/**', {base: 'public'}).pipe(dest('dist'))
    }const serve = () => {watch('src/assets/styles/*.scss', style)watch('src/assets/scripts/*.js', script)watch('src/*.html', page)watch(['src/assets/images/**','src/assets/fonts/**','public/**'], bs.reload)bs.init({notify: false,port: 2080,open: false,// files: 'temp/**',server: {baseDir: ['temp', 'src', 'public'], // 按顺序查找routes: {'/node_modules': 'node_modules'}}})
    }const useref = () => {return src('temp/*.html', { base: 'temp' }).pipe(plugins.useref({ searchPath: ['temp', '.'] })).pipe(plugins.if(/\.js$/, plugins.uglify())).pipe(plugins.if(/\.css$/, plugins.cleanCss())).pipe(plugins.if(/\.html$/, plugins.htmlmin({collapseWhitespace: true,minifyCSS: true,minifyJS: true}))).pipe(dest('dist'))
    }// const compile = parallel(style, script, page, image, font)
    const compile = parallel(style, script, page)// 上线之前执行的任务
    const build = series(clean,parallel(series(compile, useref),image,font,extra)
    )// 开发阶段
    const develop = series(compile, serve)module.exports = {clean,compile,build,develop,
    }
    

    其中依赖文件如下:

    "devDependencies": {"@babel/core": "^7.10.2","@babel/preset-env": "^7.10.2","browser-sync": "^2.26.7","del": "^5.1.0","gulp": "^4.0.2","gulp-babel": "^8.0.0","gulp-clean-css": "^4.3.0","gulp-htmlmin": "^5.0.1","gulp-if": "^3.0.0","gulp-imagemin": "^7.1.0","gulp-load-plugins": "^2.0.3","gulp-sass": "^4.1.0","gulp-swig": "^0.9.1","gulp-uglify": "^3.0.2","gulp-useref": "^4.0.1"},
    
  • Gulp补充

4. FIS

FIS是百度的前端团队推出的构建系统,FIS相对于前两种微内核的特点,它更像是一种捆绑套餐,它把我们的需求都尽可能的集成在内部了,例如资源加载、模块化开发、代码部署、甚至是性能优化。正式因为FIS的大而全,所以在国内流行。FIS适合初学者。

全局安装:yarn global add fis3

执行fis3 release

大前端学习--开发脚手架与自动化构建工作流封装相关推荐

  1. 开发脚手架与自动化构建

    一.前端工程化 概念:遵循一定的标准和规范,通过工具去提高效率,降低成本的一种手段 1.遇到的主要问题 想要使用ES6+新特性,但是兼容有问题 想要使用Less/Sass/PostCSS增强CSS的编 ...

  2. 大前端学习2-1__脚手架工具

    脚手架工具 脚手架工具 脚手架工具介绍 常用的及脚手架工具 Yeoman Sub Generator 常用的Yeoman使用步骤 自定义Generator 根据模板生成文件 接收用户输入 vue Ge ...

  3. 前端工程化——脚手架及自动化构建

    定义 一切以提高效率.降低成本.质量保证为目的的手段,都属于工程化 前端工程化主要解决的问题 传统语言或语法的弊端 无法使用模块化/组件化 重复性的机械工作 代码风格统一.质量保证 依赖后端服务接口的 ...

  4. 靠在校所学的前端知识,你可能连实习都找不到,附【大前端学习路线】

    又是一年毕业季,又有万千学子开始涌入社会这片汪洋. 前些日子有个大学生小伙问了我关于前端开发找工作的问题,他说他很迷茫,大家都找到了工作,自己的简历投了却杳无音信,于是来问我是不是哪些环节没有做好. ...

  5. 前端H5怎么切换语言_「自学系列一」HTML5大前端学习路线+视频教程完整版

    全新Java.HTML5前端.大数据.Python爬虫.全链UI设计.软件测试.Unity 3D.Go语言等多个技术方向的全套视频. 面对这么多的知识点,有的盆友就麻爪了-- 我是谁? 我该从哪里开始 ...

  6. 开发脚手架及封装自动化构建工作流

    前端工程化 概述 1.多人协作开发,无法硬性统一大家的代码风格 从仓库中pull回来的代码质量无法保证 2.部分功能开发时需要等待后端服务接口提前完成 3.传统语言或语法的弊端 4.无法使用模块化/组 ...

  7. 大前端学习记二开发准备

    此学习笔记主要是根据XX网大前端课程学习时的笔记整理. 目录 一.项目开发准备 1.1 开发环境搭建 1.1.1 虚拟机介绍 1.1.2 Vue-Cli 1.2 Linux中常见指令 1.3 Dock ...

  8. html5网页制作代码_好程序员HTML5大前端常用开发工具大集合

    好程序员HTML5大前端分享常用开发工具大集合HTML5作为当前最为流行的编程语言,广为适用.语言的使用人数急剧增长,更多地开发人员使用这种语言来创建各种内容并放到互联网上.随着每一个新版本的发布,H ...

  9. 【自学系列一】HTML5大前端学习路线+视频教程(完整版)

    今年,本公司全新发布了囊括Java.HTML5前端.大数据.Python爬虫.全链UI设计.软件测试.Unity 3D.Go语言等多个技术方向的全套视频. 面对这么多的知识点,有的盆友就麻爪了-- 我 ...

最新文章

  1. MySQL用sqoop导出乱码_Sqoop将hive数据导出到MySQL中文乱码了怎么办?
  2. Spring MVC 如何加载静态html
  3. include require区别
  4. gmail怎么延时发送邮件呢?
  5. 项目学生:Web服务集成
  6. c语言课程设计2018,C语言课程设计报告(2018)——学生管理系统(17页)-原创力文档...
  7. Software Defined Networking(Week 2, part 2)
  8. 《流畅的Python》读书笔记——Python对象引用、可变性和垃圾回收
  9. 2014年07月21日
  10. access 跳过一次for循环_Java中的循环结构
  11. linux 命令学习
  12. 实践 | 打印机扫描文件到电脑
  13. 教你如何把qlv转换成mp4格式
  14. Android IBeacon
  15. python爬虫之爬取招聘岗位信息
  16. 中国节水灌溉设备产业运行分析与投资前景规划报告2022年版
  17. java使用flex生成swf_flex动态生成矢量swf字体--java动态生成swf文件
  18. 【设计模式】2.工厂模式
  19. 【转】NAS群晖DSM 5.0-4458安装教程
  20. ipv4到ipv6过渡的三种方案

热门文章

  1. [唐诗]古风(其十九)-李白
  2. 人工智能在医药行业的应用
  3. 什么是软链接、硬链接
  4. 【正点原子FPGA连载】第十二章呼吸灯实验 -摘自【正点原子】新起点之FPGA开发指南_V2.1
  5. 阿里云ACP云计算错题集1-40
  6. nm 和 c++filt - [GNU/Linux]
  7. 学生云服务器哪个好?阿里云,腾讯云,华为云,有适合学生党云服务器推荐吗?
  8. java如何获取网页全部内容
  9. CentOS 7 下的软件安装方法及策略
  10. 沉没成本:为什么该放手时我们总是无法放手